Page MenuHomePhorge

D2894.1775168100.diff
No OneTemporary

Authored By
Unknown
Size
91 KB
Referenced Files
None
Subscribers
None

D2894.1775168100.diff

diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -114,6 +114,17 @@
return $this->morphOne('App\Entitlement', 'entitleable');
}
+ /**
+ * Entitlements for this domain.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function entitlements()
+ {
+ return $this->hasMany('App\Entitlement', 'entitleable_id', 'id')
+ ->where('entitleable_type', Domain::class);
+ }
+
/**
* Return list of public+active domain names (for current tenant)
*/
diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php
--- a/src/app/Entitlement.php
+++ b/src/app/Entitlement.php
@@ -118,6 +118,33 @@
return $this->entitleable->email;
}
+ /**
+ * Simplified Entitlement/SKU information for a specified entitleable object
+ *
+ * @param object $object Entitleable object
+ *
+ * @return array Skus list with some metadata
+ */
+ public static function objectEntitlementsSummary($object): array
+ {
+ $skus = [];
+
+ // TODO: I agree this format may need to be extended in future
+
+ foreach ($object->entitlements as $ent) {
+ $sku = $ent->sku;
+
+ if (!isset($skus[$sku->id])) {
+ $skus[$sku->id] = ['costs' => [], 'count' => 0];
+ }
+
+ $skus[$sku->id]['count']++;
+ $skus[$sku->id]['costs'][] = $ent->cost;
+ }
+
+ return $skus;
+ }
+
/**
* The SKU concerned.
*
diff --git a/src/app/Handlers/Base.php b/src/app/Handlers/Base.php
--- a/src/app/Handlers/Base.php
+++ b/src/app/Handlers/Base.php
@@ -16,18 +16,18 @@
/**
* Check if the SKU is available to the user. An SKU is available
- * to the user when either it is active or there's already an
+ * to the user/domain when either it is active or there's already an
* active entitlement.
*
- * @param \App\Sku $sku The SKU object
- * @param \App\User $user The user object
+ * @param \App\Sku $sku The SKU object
+ * @param \App\User|\App\Domain $object The user or domain object
*
* @return bool
*/
- public static function isAvailable(\App\Sku $sku, \App\User $user): bool
+ public static function isAvailable(\App\Sku $sku, $object): bool
{
if (!$sku->active) {
- if (!$user->entitlements()->where('sku_id', $sku->id)->first()) {
+ if (!$object->entitlements()->where('sku_id', $sku->id)->first()) {
return false;
}
}
diff --git a/src/app/Handlers/Beta/Base.php b/src/app/Handlers/Beta/Base.php
--- a/src/app/Handlers/Beta/Base.php
+++ b/src/app/Handlers/Beta/Base.php
@@ -5,23 +5,27 @@
class Base extends \App\Handlers\Base
{
/**
- * Check if the SKU is available to the user.
+ * Check if the SKU is available to the user/domain.
*
- * @param \App\Sku $sku The SKU object
- * @param \App\User $user The user object
+ * @param \App\Sku $sku The SKU object
+ * @param \App\User|\App\Domain $object The user or domain object
*
* @return bool
*/
- public static function isAvailable(\App\Sku $sku, \App\User $user): bool
+ public static function isAvailable(\App\Sku $sku, $object): bool
{
// These SKUs must be:
// 1) already assigned or
// 2) active and a 'beta' entitlement must exist.
+ if (!$object instanceof \App\User) {
+ return false;
+ }
+
if ($sku->active) {
- return $user->hasSku('beta');
+ return $object->hasSku('beta');
} else {
- if ($user->entitlements()->where('sku_id', $sku->id)->first()) {
+ if ($object->entitlements()->where('sku_id', $sku->id)->first()) {
return true;
}
}
diff --git a/src/app/Handlers/Distlist.php b/src/app/Handlers/Distlist.php
--- a/src/app/Handlers/Distlist.php
+++ b/src/app/Handlers/Distlist.php
@@ -15,21 +15,21 @@
}
/**
- * Check if the SKU is available to the user.
+ * Check if the SKU is available to the user/domain.
*
- * @param \App\Sku $sku The SKU object
- * @param \App\User $user The user object
+ * @param \App\Sku $sku The SKU object
+ * @param \App\User|\App\Domain $object The user or domain object
*
* @return bool
*/
- public static function isAvailable(\App\Sku $sku, \App\User $user): bool
+ public static function isAvailable(\App\Sku $sku, $object): bool
{
// This SKU must be:
// - already assigned, or active and a 'beta' entitlement must exist
// - and this is a group account owner (custom domain)
- if (parent::isAvailable($sku, $user)) {
- return $user->wallet()->entitlements()
+ if (parent::isAvailable($sku, $object)) {
+ return $object->wallet()->entitlements()
->where('entitleable_type', \App\Domain::class)->count() > 0;
}
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -54,6 +54,18 @@
return response()->json($result);
}
+ /**
+ * Create a domain.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function store(Request $request)
+ {
+ return $this->errorResponse(404);
+ }
+
/**
* Suspend the domain
*
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -5,8 +5,11 @@
use App\Domain;
use App\Http\Controllers\Controller;
use App\Backends\LDAP;
+use App\Rules\UserEmailDomain;
use Carbon\Carbon;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Validator;
class DomainsController extends Controller
{
@@ -28,6 +31,10 @@
}
}
+ usort($list, function ($a, $b) {
+ return strcmp($a['namespace'], $b['namespace']);
+ });
+
return response()->json($list);
}
@@ -130,9 +137,8 @@
]);
}
-
/**
- * Store a newly created resource in storage.
+ * Create a domain.
*
* @param \Illuminate\Http\Request $request
*
@@ -140,7 +146,61 @@
*/
public function store(Request $request)
{
- return $this->errorResponse(404);
+ $current_user = $this->guard()->user();
+ $owner = $current_user->wallet()->owner;
+
+ if ($owner->id != $current_user->id) {
+ return $this->errorResponse(403);
+ }
+
+ // Validate the input
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'namespace' => ['required', 'string', new UserEmailDomain()]
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ $namespace = \strtolower(request()->input('namespace'));
+
+ // Domain already exists
+ if (Domain::withTrashed()->where('namespace', $namespace)->exists()) {
+ $errors = ['namespace' => \trans('validation.domainnotavailable')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) {
+ $errors = ['package' => \trans('validation.packagerequired')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ if (!$package->isDomain()) {
+ $errors = ['package' => \trans('validation.packageinvalid')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ DB::beginTransaction();
+
+ // TODO: Force-delete domain if it is soft-deleted and belongs to the same user
+
+ // Create the domain
+ $domain = Domain::create([
+ 'namespace' => $namespace,
+ 'type' => \App\Domain::TYPE_EXTERNAL,
+ ]);
+
+ $domain->assignPackage($package, $owner);
+
+ DB::commit();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => __('app.domain-create-success'),
+ ]);
}
/**
@@ -179,8 +239,19 @@
// Status info
$response['statusInfo'] = self::statusInfo($domain);
+ // Entitlements info
+ $response['skus'] = \App\Entitlement::objectEntitlementsSummary($domain);
+
$response = array_merge($response, self::domainStatuses($domain));
+ // Some basic information about the domain wallet
+ $wallet = $domain->wallet();
+ $response['wallet'] = $wallet->toArray();
+ if ($wallet->discount) {
+ $response['wallet']['discount'] = $wallet->discount->discount;
+ $response['wallet']['discount_description'] = $wallet->discount->description;
+ }
+
return response()->json($response);
}
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -32,6 +32,28 @@
return $this->errorResponse(404);
}
+ /**
+ * Get a list of SKUs available to the domain.
+ *
+ * @param int $id Domain identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function domainSkus($id)
+ {
+ $domain = \App\Domain::find($id);
+
+ if (!$this->checkTenant($domain)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canRead($domain)) {
+ return $this->errorResponse(403);
+ }
+
+ return $this->objectSkus($domain);
+ }
+
/**
* Show the form for editing the specified sku.
*
@@ -129,23 +151,35 @@
return $this->errorResponse(403);
}
- $type = request()->input('type');
+ return $this->objectSkus($user);
+ }
+
+ /**
+ * Return SKUs available to the specified user/domain.
+ *
+ * @param object $object User or Domain object
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ protected static function objectSkus($object)
+ {
+ $type = $object instanceof \App\Domain ? 'domain' : 'user';
$response = [];
// Note: Order by title for consistent ordering in tests
- $skus = Sku::withObjectTenantContext($user)->orderBy('title')->get();
+ $skus = Sku::withObjectTenantContext($object)->orderBy('title')->get();
foreach ($skus as $sku) {
if (!class_exists($sku->handler_class)) {
continue;
}
- if (!$sku->handler_class::isAvailable($sku, $user)) {
+ if (!$sku->handler_class::isAvailable($sku, $object)) {
continue;
}
- if ($data = $this->skuElement($sku)) {
- if ($type && $type != $data['type']) {
+ if ($data = self::skuElement($sku)) {
+ if ($type != $data['type']) {
continue;
}
@@ -168,7 +202,7 @@
*
* @return array|null Metadata
*/
- protected function skuElement($sku): ?array
+ protected static function skuElement($sku): ?array
{
if (!class_exists($sku->handler_class)) {
return null;
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -138,18 +138,7 @@
$response = $this->userResponse($user);
- // Simplified Entitlement/SKU information,
- // TODO: I agree this format may need to be extended in future
- $response['skus'] = [];
- foreach ($user->entitlements as $ent) {
- $sku = $ent->sku;
- if (!isset($response['skus'][$sku->id])) {
- $response['skus'][$sku->id] = ['costs' => [], 'count' => 0];
- }
- $response['skus'][$sku->id]['count']++;
- $response['skus'][$sku->id]['costs'][] = $ent->cost;
- }
-
+ $response['skus'] = \App\Entitlement::objectEntitlementsSummary($user);
$response['config'] = $user->getConfig();
return response()->json($response);
diff --git a/src/app/Rules/UserEmailDomain.php b/src/app/Rules/UserEmailDomain.php
--- a/src/app/Rules/UserEmailDomain.php
+++ b/src/app/Rules/UserEmailDomain.php
@@ -24,7 +24,7 @@
/**
* Determine if the validation rule passes.
*
- * Validation of local part of an email address that's
+ * Validation of a domain part of an email address that's
* going to be user's login.
*
* @param string $attribute Attribute name
@@ -34,8 +34,19 @@
*/
public function passes($attribute, $domain): bool
{
- // don't allow @localhost and other no-fqdn
- if (empty($domain) || strpos($domain, '.') === false || stripos($domain, 'www.') === 0) {
+ // don't allow @localhost and other non-fqdn
+ if (
+ empty($domain)
+ || !is_string($domain)
+ || strpos($domain, '.') === false
+ || stripos($domain, 'www.') === 0
+ ) {
+ $this->message = \trans('validation.domaininvalid');
+ return false;
+ }
+
+ // Check the max length, according to the database column length
+ if (strlen($domain) > 191) {
$this->message = \trans('validation.domaininvalid');
return false;
}
diff --git a/src/app/Rules/UserEmailLocal.php b/src/app/Rules/UserEmailLocal.php
--- a/src/app/Rules/UserEmailLocal.php
+++ b/src/app/Rules/UserEmailLocal.php
@@ -34,7 +34,11 @@
public function passes($attribute, $login): bool
{
// Strict validation
- if (!preg_match('/^[A-Za-z0-9_.-]+$/', $login)) {
+ if (
+ empty($login)
+ || !is_string($login)
+ || !preg_match('/^[A-Za-z0-9_.-]+$/', $login)
+ ) {
$this->message = \trans('validation.entryinvalid', ['attribute' => $attribute]);
return false;
}
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -400,6 +400,22 @@
return this.$t('status.active')
},
+ // Append some wallet properties to the object
+ userWalletProps(object) {
+ let wallet = store.state.authInfo.accounts[0]
+
+ if (!wallet) {
+ wallet = store.state.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
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -41,6 +41,7 @@
'distlist-suspend-success' => 'Distribution list suspended successfully.',
'distlist-unsuspend-success' => 'Distribution list unsuspended successfully.',
+ 'domain-create-success' => 'Domain created successfully.',
'domain-verify-success' => 'Domain verified successfully.',
'domain-verify-error' => 'Domain ownership verification failed.',
'domain-suspend-success' => 'Domain suspended successfully.',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -79,6 +79,9 @@
'config-intro' => "In order to let {app} receive email traffic for your domain you need to adjust the DNS settings, more precisely the MX entries, accordingly.",
'config-sample' => "Edit your domain's zone file and replace existing MX entries with the following values:",
'config-hint' => "If you don't know how to set DNS entries for your domain, please contact the registration service where you registered the domain or your web hosting provider.",
+ 'create' => "Create domain",
+ 'name' => "Name",
+ 'new' => "New domain",
],
'error' => [
diff --git a/src/resources/themes/forms.scss b/src/resources/themes/forms.scss
--- a/src/resources/themes/forms.scss
+++ b/src/resources/themes/forms.scss
@@ -80,9 +80,6 @@
}
.nav-tabs {
- flex-wrap: nowrap;
- overflow-x: auto;
-
.nav-link {
white-space: nowrap;
padding: 0.5rem 0.75rem;
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -1,23 +1,19 @@
<template>
<div class="container">
- <status-component :status="status" @status-update="statusUpdate"></status-component>
+ <status-component v-if="domain_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
- <div v-if="domain" class="card">
+ <div class="card">
<div class="card-body">
- <div class="card-title">{{ domain.namespace }}</div>
+ <div class="card-title" v-if="domain_id === 'new'">{{ $t('domain.new') }}</div>
+ <div class="card-title" v-else>{{ $t('form.domain') }}</div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
- <li class="nav-item" v-if="!domain.isConfirmed">
- <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
- {{ $t('domain.verify') }}
- </a>
- </li>
- <li class="nav-item" v-if="domain.isConfirmed">
+ <li class="nav-item">
<a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
- {{ $t('domain.config') }}
+ {{ $t('form.general') }}
</a>
</li>
- <li class="nav-item">
+ <li class="nav-item" v-if="domain.id">
<a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
@@ -25,7 +21,34 @@
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
- <div v-if="!domain.isConfirmed" class="card-body" id="domain-verify">
+ <form @submit.prevent="submit" class="card-body">
+ <div v-if="domain.id" class="row plaintext mb-3">
+ <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
+ <div class="col-sm-8">
+ <span :class="$root.domainStatusClass(domain) + ' form-control-plaintext'" id="status">{{ $root.domainStatusText(domain) }}</span>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="name" class="col-sm-4 col-form-label">{{ $t('domain.name') }}</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="namespace" v-model="domain.namespace" :disabled="domain.id">
+ </div>
+ </div>
+ <div v-if="!domain.id" id="domain-packages" class="row">
+ <label class="col-sm-4 col-form-label">{{ $t('user.package') }}</label>
+ <package-select class="col-sm-8 pt-sm-1" type="domain"></package-select>
+ </div>
+ <div v-if="domain.id" id="domain-skus" class="row">
+ <label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
+ <subscription-select v-if="domain.id" class="col-sm-8 pt-sm-1" type="domain" :object="domain" :readonly="true"></subscription-select>
+ </div>
+ <button v-if="!domain.id" class="btn btn-primary mt-3" type="submit">
+ <svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
+ </button>
+ </form>
+ <hr class="m-0" v-if="domain.id">
+ <div v-if="domain.id && !domain.isConfirmed" class="card-body" id="domain-verify">
+ <h5 class="mb-3">{{ $t('domain.verify') }}</h5>
<div class="card-text">
<p>{{ $t('domain.verify-intro') }}</p>
<p>
@@ -41,6 +64,7 @@
</div>
</div>
<div v-if="domain.isConfirmed" class="card-body" id="domain-config">
+ <h5 class="mb-3">{{ $t('domain.config') }}</h5>
<div class="card-text">
<p>{{ $t('domain.config-intro', { app: $root.appName }) }}</p>
<p>{{ $t('domain.config-sample') }} <pre>{{ domain.mx.join("\n") }}</pre></p>
@@ -74,23 +98,29 @@
<script>
import ListInput from '../Widgets/ListInput'
+ import PackageSelect from '../Widgets/PackageSelect'
import StatusComponent from '../Widgets/Status'
+ import SubscriptionSelect from '../Widgets/SubscriptionSelect'
export default {
components: {
ListInput,
- StatusComponent
+ PackageSelect,
+ StatusComponent,
+ SubscriptionSelect
},
data() {
return {
domain_id: null,
- domain: null,
+ domain: {},
spf_whitelist: [],
status: {}
}
},
created() {
- if (this.domain_id = this.$route.params.domain) {
+ this.domain_id = this.$route.params.domain
+
+ if (this.domain_id !== 'new') {
this.$root.startLoading()
axios.get('/api/v4/domains/' + this.domain_id)
@@ -106,10 +136,11 @@
this.status = response.data.statusInfo
})
.catch(this.$root.errorHandler)
- } else {
- this.$root.errorPage(404)
}
},
+ mounted() {
+ $('#namespace').focus()
+ },
methods: {
confirm() {
axios.get('/api/v4/domains/' + this.domain_id + '/confirm')
@@ -127,6 +158,20 @@
statusUpdate(domain) {
this.domain = Object.assign({}, this.domain, domain)
},
+ submit() {
+ this.$root.clearFormValidation($('#general form'))
+
+ let method = 'post'
+ let location = '/api/v4/domains'
+
+ this.domain.package = $('#domain-packages input:checked').val()
+
+ axios[method](location, this.domain)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ this.$router.push({ name: 'domains' })
+ })
+ },
submitSettings() {
this.$root.clearFormValidation($('#settings form'))
diff --git a/src/resources/vue/Domain/List.vue b/src/resources/vue/Domain/List.vue
--- a/src/resources/vue/Domain/List.vue
+++ b/src/resources/vue/Domain/List.vue
@@ -2,7 +2,12 @@
<div class="container">
<div class="card" id="domain-list">
<div class="card-body">
- <div class="card-title">{{ $t('user.domains') }}</div>
+ <div class="card-title">
+ {{ $t('user.domains') }}
+ <router-link class="btn btn-success float-end create-domain" :to="{ path: 'domain/new' }" tag="button">
+ <svg-icon icon="globe"></svg-icon> {{ $t('domain.create') }}
+ </router-link>
+ </div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -78,98 +78,12 @@
</div>
</div>
<div v-if="user_id === 'new'" id="user-packages" class="row mb-3">
- <label class="col-sm-4 col-form-label">Package</label>
- <div class="col-sm-8">
- <table class="table table-sm form-list">
- <thead class="visually-hidden">
- <tr>
- <th scope="col"></th>
- <th scope="col">{{ $t('user.package') }}</th>
- <th scope="col">{{ $t('user.price') }}</th>
- <th scope="col"></th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="pkg in packages" :id="'p' + pkg.id" :key="pkg.id">
- <td class="selection">
- <input type="checkbox" @click="selectPackage"
- :value="pkg.id"
- :checked="pkg.id == package_id"
- :id="'pkg-input-' + pkg.id"
- >
- </td>
- <td class="name">
- <label :for="'pkg-input-' + pkg.id">{{ pkg.name }}</label>
- </td>
- <td class="price text-nowrap">
- {{ $root.priceLabel(pkg.cost, discount, currency) }}
- </td>
- <td class="buttons">
- <button v-if="pkg.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip="pkg.description">
- <svg-icon icon="info-circle"></svg-icon>
- <span class="visually-hidden">{{ $t('btn.moreinfo') }}</span>
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- <small v-if="discount > 0" class="hint">
- <hr class="m-0">
- &sup1; {{ $t('user.discount-hint') }}: {{ discount }}% - {{ discount_description }}
- </small>
- </div>
+ <label class="col-sm-4 col-form-label">{{ $t('user.package') }}</label>
+ <package-select class="col-sm-8 pt-sm-1"></package-select>
</div>
<div v-if="user_id !== 'new'" id="user-skus" class="row mb-3">
<label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
- <div class="col-sm-8">
- <table class="table table-sm form-list">
- <thead class="visually-hidden">
- <tr>
- <th scope="col"></th>
- <th scope="col">{{ $t('user.subscription') }}</th>
- <th scope="col">{{ $t('user.price') }}</th>
- <th scope="col"></th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="sku in skus" :id="'s' + sku.id" :key="sku.id">
- <td class="selection">
- <input type="checkbox" @input="onInputSku"
- :value="sku.id"
- :disabled="sku.readonly"
- :checked="sku.enabled"
- :id="'sku-input-' + sku.title"
- >
- </td>
- <td class="name">
- <label :for="'sku-input-' + sku.title">{{ sku.name }}</label>
- <div v-if="sku.range" class="range-input">
- <label class="text-nowrap">{{ sku.range.min }} {{ sku.range.unit }}</label>
- <input
- type="range" class="form-range" @input="rangeUpdate"
- :value="sku.value || sku.range.min"
- :min="sku.range.min"
- :max="sku.range.max"
- >
- </div>
- </td>
- <td class="price text-nowrap">
- {{ $root.priceLabel(sku.cost, discount, currency) }}
- </td>
- <td class="buttons">
- <button v-if="sku.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip="sku.description">
- <svg-icon icon="info-circle"></svg-icon>
- <span class="visually-hidden">{{ $t('btn.moreinfo') }}</span>
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- <small v-if="discount > 0" class="hint">
- <hr class="m-0">
- &sup1; {{ $t('user.discount-hint') }}: {{ discount }}% - {{ discount_description }}
- </small>
- </div>
+ <subscription-select v-if="user.id" class="col-sm-8 pt-sm-1" :object="user"></subscription-select>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
@@ -217,57 +131,30 @@
<script>
import { Modal } from 'bootstrap'
import ListInput from '../Widgets/ListInput'
+ import PackageSelect from '../Widgets/PackageSelect'
import StatusComponent from '../Widgets/Status'
+ import SubscriptionSelect from '../Widgets/SubscriptionSelect'
export default {
components: {
ListInput,
- StatusComponent
+ PackageSelect,
+ StatusComponent,
+ SubscriptionSelect
},
data() {
return {
- currency: '',
- discount: 0,
- discount_description: '',
user_id: null,
user: { aliases: [], config: [] },
- packages: [],
- package_id: null,
- skus: [],
status: {}
}
},
created() {
this.user_id = this.$route.params.user
- let wallet = this.$store.state.authInfo.accounts[0]
-
- if (!wallet) {
- wallet = this.$store.state.authInfo.wallets[0]
- }
-
- if (wallet) {
- this.currency = wallet.currency
- if (wallet.discount) {
- this.discount = wallet.discount
- this.discount_description = wallet.discount_description
- }
- }
-
- this.$root.startLoading()
+ if (this.user_id !== 'new') {
+ this.$root.startLoading()
- if (this.user_id === 'new') {
- // do nothing (for now)
- axios.get('/api/v4/packages')
- .then(response => {
- this.$root.stopLoading()
-
- this.packages = response.data.filter(pkg => !pkg.isDomain)
- this.package_id = this.packages[0].id
- })
- .catch(this.$root.errorHandler)
- }
- else {
axios.get('/api/v4/users/' + this.user_id)
.then(response => {
this.$root.stopLoading()
@@ -276,35 +163,7 @@
this.user.first_name = response.data.settings.first_name
this.user.last_name = response.data.settings.last_name
this.user.organization = response.data.settings.organization
- this.discount = this.user.wallet.discount
- this.discount_description = this.user.wallet.discount_description
this.status = response.data.statusInfo
-
- axios.get('/api/v4/users/' + this.user_id + '/skus?type=user')
- .then(response => {
- // "merge" SKUs with user entitlement-SKUs
- this.skus = response.data
- .map(sku => {
- const userSku = this.user.skus[sku.id]
- if (userSku) {
- sku.enabled = true
- sku.skuCost = sku.cost
- sku.cost = userSku.costs.reduce((sum, current) => sum + current)
- sku.value = userSku.count
- sku.costs = userSku.costs
- } else if (!sku.readonly) {
- sku.enabled = false
- }
-
- return sku
- })
-
- // Update all range inputs (and price)
- this.$nextTick(() => {
- $('#user-skus input[type=range]').each((idx, elem) => { this.rangeUpdate(elem) })
- })
- })
- .catch(this.$root.errorHandler)
})
.catch(this.$root.errorHandler)
}
@@ -317,7 +176,7 @@
},
methods: {
submit() {
- this.$root.clearFormValidation($('#user-info form'))
+ this.$root.clearFormValidation($('#general form'))
let method = 'post'
let location = '/api/v4/users'
@@ -335,7 +194,7 @@
})
this.user.skus = skus
} else {
- this.user.package = this.package_id
+ this.user.package = $('#user-packages input:checked').val()
}
axios[method](location, this.user)
@@ -357,106 +216,6 @@
this.$toast.success(response.data.message)
})
},
- onInputSku(e) {
- let input = e.target
- let sku = this.findSku(input.value)
- let required = []
-
- // We use 'readonly', not 'disabled', because we might want to handle
- // input events. For example to display an error when someone clicks
- // the locked input
- if (input.readOnly) {
- input.checked = !input.checked
- // TODO: Display an alert explaining why it's locked
- return
- }
-
- // TODO: Following code might not work if we change definition of forbidden/required
- // or we just need more sophisticated SKU dependency rules
-
- if (input.checked) {
- // Check if a required SKU is selected, alert the user if not
- (sku.required || []).forEach(title => {
- this.skus.forEach(item => {
- let checkbox
- if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
- if (!checkbox.checked) {
- required.push(item.name)
- }
- }
- })
- })
-
- if (required.length) {
- input.checked = false
- return alert(this.$t('user.skureq', { sku: sku.name, list: required.join(', ') }))
- }
- } else {
- // Uncheck all dependent SKUs, e.g. when unchecking Groupware we also uncheck Activesync
- // TODO: Should we display an alert instead?
- this.skus.forEach(item => {
- if (item.required && item.required.indexOf(sku.handler) > -1) {
- $('#s' + item.id).find('input[type=checkbox]').prop('checked', false)
- }
- })
- }
-
- // Uncheck+lock/unlock conflicting SKUs
- (sku.forbidden || []).forEach(title => {
- this.skus.forEach(item => {
- let checkbox
- if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
- if (input.checked) {
- checkbox.checked = false
- checkbox.readOnly = true
- } else {
- checkbox.readOnly = false
- }
- }
- })
- })
- },
- selectPackage(e) {
- // Make sure there always is only one package selected
- $('#user-packages input').prop('checked', false)
- this.package_id = $(e.target).prop('checked', false).val()
- },
- rangeUpdate(e) {
- let input = $(e.target || e)
- let value = input.val()
- let record = input.parents('tr').first()
- let sku_id = record.find('input[type=checkbox]').val()
- let sku = this.findSku(sku_id)
- let existing = sku.costs ? sku.costs.length : 0
- let cost
-
- // Calculate cost, considering both existing entitlement cost and sku cost
- if (existing) {
- cost = sku.costs
- .sort((a, b) => a - b) // sort by cost ascending (free units first)
- .slice(0, value)
- .reduce((sum, current) => sum + current)
-
- if (value > existing) {
- cost += sku.skuCost * (value - existing)
- }
- } else {
- cost = sku.cost * (value - sku.units_free)
- }
-
- // Update the label
- input.prev().text(value + ' ' + sku.range.unit)
-
- // Update the price
- record.find('.price').text(this.$root.priceLabel(cost, this.discount, this.currency))
- },
- findSku(id) {
- for (let i = 0; i < this.skus.length; i++) {
- if (this.skus[i].id == id) {
- return this.skus[i];
- }
- }
- },
statusUpdate(user) {
this.user = Object.assign({}, this.user, user)
},
diff --git a/src/resources/vue/Widgets/PackageSelect.vue b/src/resources/vue/Widgets/PackageSelect.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/PackageSelect.vue
@@ -0,0 +1,87 @@
+<template>
+ <div>
+ <table class="table table-sm form-list">
+ <thead class="visually-hidden">
+ <tr>
+ <th scope="col"></th>
+ <th scope="col">{{ $t('user.package') }}</th>
+ <th scope="col">{{ $t('user.price') }}</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="pkg in packages" :id="'p' + pkg.id" :key="pkg.id">
+ <td class="selection">
+ <input type="checkbox" @change="selectPackage"
+ :value="pkg.id"
+ :checked="pkg.id == package_id"
+ :readonly="pkg.id == package_id"
+ :id="'pkg-input-' + pkg.id"
+ >
+ </td>
+ <td class="name">
+ <label :for="'pkg-input-' + pkg.id">{{ pkg.name }}</label>
+ </td>
+ <td class="price text-nowrap">
+ {{ $root.priceLabel(pkg.cost, discount, currency) }}
+ </td>
+ <td class="buttons">
+ <button v-if="pkg.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip="pkg.description">
+ <svg-icon icon="info-circle"></svg-icon>
+ <span class="visually-hidden">{{ $t('btn.moreinfo') }}</span>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <small v-if="discount > 0" class="hint">
+ <hr class="m-0 mt-1">
+ &sup1; {{ $t('user.discount-hint') }}: {{ discount }}% - {{ discount_description }}
+ </small>
+ </div>
+</template>
+
+<script>
+ export default {
+ props: {
+ type: { type: String, default: 'user' }
+ },
+ data() {
+ return {
+ currency: '',
+ discount: 0,
+ discount_description: '',
+ packages: [],
+ package_id: null
+ }
+ },
+ created() {
+ // assign currency, discount, discount_description of the current user
+ this.$root.userWalletProps(this)
+
+ this.$root.startLoading()
+
+ axios.get('/api/v4/packages')
+ .then(response => {
+ this.$root.stopLoading()
+
+ this.packages = response.data.filter(pkg => {
+ if (this.type == 'domain') {
+ return pkg.isDomain
+ }
+
+ return !pkg.isDomain
+ })
+ this.package_id = this.packages[0].id
+ })
+ .catch(this.$root.errorHandler)
+ },
+ methods: {
+ selectPackage(e) {
+ // Make sure there always is one package selected
+ $(this.$el).find('input').not(e.target).prop('checked', false)
+ this.package_id = $(e.target).prop('checked', true).val()
+ },
+ }
+ }
+</script>
diff --git a/src/resources/vue/Widgets/SubscriptionSelect.vue b/src/resources/vue/Widgets/SubscriptionSelect.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/SubscriptionSelect.vue
@@ -0,0 +1,208 @@
+<template>
+ <div>
+ <table class="table table-sm form-list">
+ <thead class="visually-hidden">
+ <tr>
+ <th scope="col"></th>
+ <th scope="col">{{ $t('user.subscription') }}</th>
+ <th scope="col">{{ $t('user.price') }}</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="sku in skus" :id="'s' + sku.id" :key="sku.id">
+ <td class="selection">
+ <input type="checkbox" @input="onInputSku"
+ :value="sku.id"
+ :disabled="sku.readonly || readonly"
+ :checked="sku.enabled"
+ :id="'sku-input-' + sku.title"
+ >
+ </td>
+ <td class="name">
+ <label :for="'sku-input-' + sku.title">{{ sku.name }}</label>
+ <div v-if="sku.range" class="range-input">
+ <label class="text-nowrap">{{ sku.range.min }} {{ sku.range.unit }}</label>
+ <input type="range" class="form-range" @input="rangeUpdate"
+ :value="sku.value || sku.range.min"
+ :min="sku.range.min"
+ :max="sku.range.max"
+ >
+ </div>
+ </td>
+ <td class="price text-nowrap">
+ {{ $root.priceLabel(sku.cost, discount, currency) }}
+ </td>
+ <td class="buttons">
+ <button v-if="sku.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip="sku.description">
+ <svg-icon icon="info-circle"></svg-icon>
+ <span class="visually-hidden">{{ $t('btn.moreinfo') }}</span>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <small v-if="discount > 0" class="hint">
+ <hr class="m-0 mt-1">
+ &sup1; {{ $t('user.discount-hint') }}: {{ discount }}% - {{ discount_description }}
+ </small>
+ </div>
+</template>
+
+<script>
+ export default {
+ props: {
+ object: { type: Object, default: () => {} },
+ readonly: { type: Boolean, default: false },
+ type: { type: String, default: 'user' }
+ },
+ data() {
+ return {
+ currency: '',
+ discount: 0,
+ discount_description: '',
+ skus: []
+ }
+ },
+ created() {
+ // assign currency, discount, discount_description of the current user
+ this.$root.userWalletProps(this)
+
+ if (this.object.wallet) {
+ this.discount = this.object.wallet.discount
+ this.discount_description = this.object.wallet.discount_description
+ }
+
+ this.$root.startLoading()
+
+ axios.get('/api/v4/' + this.type + 's/' + this.object.id + '/skus')
+ .then(response => {
+ this.$root.stopLoading()
+
+ if (this.readonly) {
+ response.data = response.data.filter(sku => { return sku.id in this.object.skus })
+ }
+
+ // "merge" SKUs with user entitlement-SKUs
+ this.skus = response.data
+ .map(sku => {
+ const objSku = this.object.skus[sku.id]
+ if (objSku) {
+ sku.enabled = true
+ sku.skuCost = sku.cost
+ sku.cost = objSku.costs.reduce((sum, current) => sum + current)
+ sku.value = objSku.count
+ sku.costs = objSku.costs
+ } else if (!sku.readonly) {
+ sku.enabled = false
+ }
+
+ return sku
+ })
+
+ // Update all range inputs (and price)
+ this.$nextTick(() => {
+ $(this.$el).find('input[type=range]').each((idx, elem) => { this.rangeUpdate(elem) })
+ })
+ })
+ .catch(this.$root.errorHandler)
+ },
+ methods: {
+ findSku(id) {
+ for (let i = 0; i < this.skus.length; i++) {
+ if (this.skus[i].id == id) {
+ return this.skus[i];
+ }
+ }
+ },
+ onInputSku(e) {
+ let input = e.target
+ let sku = this.findSku(input.value)
+ let required = []
+
+ // We use 'readonly', not 'disabled', because we might want to handle
+ // input events. For example to display an error when someone clicks
+ // the locked input
+ if (input.readOnly) {
+ input.checked = !input.checked
+ // TODO: Display an alert explaining why it's locked
+ return
+ }
+
+ // TODO: Following code might not work if we change definition of forbidden/required
+ // or we just need more sophisticated SKU dependency rules
+
+ if (input.checked) {
+ // Check if a required SKU is selected, alert the user if not
+ (sku.required || []).forEach(title => {
+ this.skus.forEach(item => {
+ let checkbox
+ if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
+ if (!checkbox.checked) {
+ required.push(item.name)
+ }
+ }
+ })
+ })
+
+ if (required.length) {
+ input.checked = false
+ return alert(this.$t('user.skureq', { sku: sku.name, list: required.join(', ') }))
+ }
+ } else {
+ // Uncheck all dependent SKUs, e.g. when unchecking Groupware we also uncheck Activesync
+ // TODO: Should we display an alert instead?
+ this.skus.forEach(item => {
+ if (item.required && item.required.indexOf(sku.handler) > -1) {
+ $('#s' + item.id).find('input[type=checkbox]').prop('checked', false)
+ }
+ })
+ }
+
+ // Uncheck+lock/unlock conflicting SKUs
+ (sku.forbidden || []).forEach(title => {
+ this.skus.forEach(item => {
+ let checkbox
+ if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
+ if (input.checked) {
+ checkbox.checked = false
+ checkbox.readOnly = true
+ } else {
+ checkbox.readOnly = false
+ }
+ }
+ })
+ })
+ },
+ rangeUpdate(e) {
+ let input = $(e.target || e)
+ let value = input.val()
+ let record = input.parents('tr').first()
+ let sku_id = record.find('input[type=checkbox]').val()
+ let sku = this.findSku(sku_id)
+ let existing = sku.costs ? sku.costs.length : 0
+ let cost
+
+ // Calculate cost, considering both existing entitlement cost and sku cost
+ if (existing) {
+ cost = sku.costs
+ .sort((a, b) => a - b) // sort by cost ascending (free units first)
+ .slice(0, value)
+ .reduce((sum, current) => sum + current)
+
+ if (value > existing) {
+ cost += sku.skuCost * (value - existing)
+ }
+ } else {
+ cost = sku.cost * (value - sku.units_free)
+ }
+
+ // Update the label
+ input.prev().text(value + ' ' + sku.range.unit)
+
+ // Update the price
+ record.find('.price').text(this.$root.priceLabel(cost, this.discount, this.currency))
+ }
+ }
+ }
+</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -72,6 +72,7 @@
Route::apiResource('domains', API\V4\DomainsController::class);
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
+ Route::get('domains/{id}/skus', 'API\V4\SkusController@domainSkus');
Route::get('domains/{id}/status', 'API\V4\DomainsController@status');
Route::post('domains/{id}/config', 'API\V4\DomainsController@setConfig');
@@ -176,6 +177,7 @@
],
function () {
Route::apiResource('domains', API\V4\Admin\DomainsController::class);
+ Route::get('domains/{id}/skus', 'API\V4\Admin\SkusController@domainSkus');
Route::post('domains/{id}/suspend', 'API\V4\Admin\DomainsController@suspend');
Route::post('domains/{id}/unsuspend', 'API\V4\Admin\DomainsController@unsuspend');
@@ -209,6 +211,7 @@
],
function () {
Route::apiResource('domains', API\V4\Reseller\DomainsController::class);
+ Route::get('domains/{id}/skus', 'API\V4\Reseller\SkusController@domainSkus');
Route::post('domains/{id}/suspend', 'API\V4\Reseller\DomainsController@suspend');
Route::post('domains/{id}/unsuspend', 'API\V4\Reseller\DomainsController@unsuspend');
diff --git a/src/tests/Browser/Admin/DomainTest.php b/src/tests/Browser/Admin/DomainTest.php
--- a/src/tests/Browser/Admin/DomainTest.php
+++ b/src/tests/Browser/Admin/DomainTest.php
@@ -20,6 +20,10 @@
public function setUp(): void
{
parent::setUp();
+
+ $this->deleteTestUser('test1@domainscontroller.com');
+ $this->deleteTestDomain('domainscontroller.com');
+
self::useAdminUrl();
}
@@ -31,6 +35,9 @@
$domain = $this->getTestDomain('kolab.org');
$domain->setSetting('spf_whitelist', null);
+ $this->deleteTestUser('test1@domainscontroller.com');
+ $this->deleteTestDomain('domainscontroller.com');
+
parent::tearDown();
}
@@ -119,6 +126,8 @@
public function testSuspendAndUnsuspend(): void
{
$this->browse(function (Browser $browser) {
+ $sku_domain = \App\Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $user = $this->getTestUser('test1@domainscontroller.com');
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW | Domain::STATUS_ACTIVE
| Domain::STATUS_LDAP_READY | Domain::STATUS_CONFIRMED
@@ -126,6 +135,13 @@
'type' => Domain::TYPE_EXTERNAL,
]);
+ \App\Entitlement::create([
+ 'wallet_id' => $user->wallets()->first()->id,
+ 'sku_id' => $sku_domain->id,
+ 'entitleable_id' => $domain->id,
+ 'entitleable_type' => Domain::class
+ ]);
+
$browser->visit(new DomainPage($domain->id))
->assertVisible('@domain-info #button-suspend')
->assertMissing('@domain-info #button-unsuspend')
diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php
--- a/src/tests/Browser/DomainTest.php
+++ b/src/tests/Browser/DomainTest.php
@@ -16,6 +16,23 @@
class DomainTest extends TestCaseDusk
{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->deleteTestDomain('testdomain.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestDomain('testdomain.com');
+ parent::tearDown();
+ }
/**
* Test domain info page (unauthenticated)
@@ -64,6 +81,24 @@
$browser->visit('/domain/' . $domain->id)
->on(new DomainInfo())
+ ->assertSeeIn('.card-title', 'Domain')
+ ->whenAvailable('@general', function ($browser) use ($domain) {
+ $browser->assertSeeIn('form div:nth-child(1) label', 'Status')
+ ->assertSeeIn('form div:nth-child(1) #status.text-danger', 'Not Ready')
+ ->assertSeeIn('form div:nth-child(2) label', 'Name')
+ ->assertValue('form div:nth-child(2) input:disabled', $domain->namespace)
+ ->assertSeeIn('form div:nth-child(3) label', 'Subscriptions');
+ })
+ ->whenAvailable('@general form div:nth-child(3) table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 1)
+ ->assertVisible('tbody tr td.selection input:checked:disabled')
+ ->assertSeeIn('tbody tr td.name', 'External Domain')
+ ->assertSeeIn('tbody tr td.price', '0,00 CHF/month')
+ ->assertTip(
+ 'tbody tr td.buttons button',
+ 'Host a domain that is externally registered'
+ );
+ })
->whenAvailable('@verify', function ($browser) use ($domain) {
$browser->assertSeeIn('pre', $domain->namespace)
->assertSeeIn('pre', $domain->hash())
@@ -75,6 +110,7 @@
->whenAvailable('@config', function ($browser) use ($domain) {
$browser->assertSeeIn('pre', $domain->namespace);
})
+ ->assertMissing('@general button[type=submit]')
->assertMissing('@verify');
// Check that confirmed domain page contains only the config box
@@ -97,7 +133,7 @@
$browser->visit('/domain/' . $domain->id)
->on(new DomainInfo())
->assertElementsCount('@nav a', 2)
- ->assertSeeIn('@nav #tab-general', 'Domain configuration')
+ ->assertSeeIn('@nav #tab-general', 'General')
->assertSeeIn('@nav #tab-settings', 'Settings')
->click('@nav #tab-settings')
->with('#settings form', function (Browser $browser) {
@@ -199,4 +235,64 @@
*/
});
}
+
+ /**
+ * Test domain creation page
+ */
+ public function testDomainCreate(): void
+ {
+ $this->browse(function ($browser) {
+ $browser->visit('/login')
+ ->on(new Home())
+ ->submitLogon('john@kolab.org', 'simple123')
+ ->visit('/domains')
+ ->on(new DomainList())
+ ->assertSeeIn('.card-title button.btn-success', 'Create domain')
+ ->click('.card-title button.btn-success')
+ ->on(new DomainInfo())
+ ->assertSeeIn('.card-title', 'New domain')
+ ->assertElementsCount('@nav li', 1)
+ ->assertSeeIn('@nav li:first-child', 'General')
+ ->whenAvailable('@general', function ($browser) {
+ $browser->assertSeeIn('form div:nth-child(1) label', 'Name')
+ ->assertValue('form div:nth-child(1) input:not(:disabled)', '')
+ ->assertFocused('form div:nth-child(1) input')
+ ->assertSeeIn('form div:nth-child(2) label', 'Package')
+ ->assertMissing('form div:nth-child(3)');
+ })
+ ->whenAvailable('@general form div:nth-child(2) table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 1)
+ ->assertVisible('tbody tr td.selection input:checked[readonly]')
+ ->assertSeeIn('tbody tr td.name', 'Domain Hosting')
+ ->assertSeeIn('tbody tr td.price', '0,00 CHF/month')
+ ->assertTip(
+ 'tbody tr td.buttons button',
+ 'Use your own, existing domain.'
+ );
+ })
+ ->assertSeeIn('@general button.btn-primary[type=submit]', 'Submit')
+ ->assertMissing('@config')
+ ->assertMissing('@verify')
+ ->assertMissing('@settings')
+ ->assertMissing('@status')
+ // Test error handling
+ ->click('button[type=submit]')
+ ->waitFor('#namespace + .invalid-feedback')
+ ->assertSeeIn('#namespace + .invalid-feedback', 'The namespace field is required.')
+ ->assertFocused('#namespace')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ ->type('@general form div:nth-child(1) input', 'testdomain..com')
+ ->click('button[type=submit]')
+ ->waitFor('#namespace + .invalid-feedback')
+ ->assertSeeIn('#namespace + .invalid-feedback', 'The specified domain is invalid.')
+ ->assertFocused('#namespace')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ // Test success
+ ->type('@general form div:nth-child(1) input', 'testdomain.com')
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Domain created successfully.')
+ ->on(new DomainList())
+ ->assertSeeIn('@table tr:nth-child(2) a', 'testdomain.com');
+ });
+ }
}
diff --git a/src/tests/Browser/Pages/DomainInfo.php b/src/tests/Browser/Pages/DomainInfo.php
--- a/src/tests/Browser/Pages/DomainInfo.php
+++ b/src/tests/Browser/Pages/DomainInfo.php
@@ -38,6 +38,7 @@
return [
'@app' => '#app',
'@config' => '#domain-config',
+ '@general' => '#general',
'@nav' => 'ul.nav-tabs',
'@settings' => '#settings',
'@status' => '#status-box',
diff --git a/src/tests/Browser/Reseller/DomainTest.php b/src/tests/Browser/Reseller/DomainTest.php
--- a/src/tests/Browser/Reseller/DomainTest.php
+++ b/src/tests/Browser/Reseller/DomainTest.php
@@ -20,6 +20,10 @@
public function setUp(): void
{
parent::setUp();
+
+ $this->deleteTestUser('test1@domainscontroller.com');
+ $this->deleteTestDomain('domainscontroller.com');
+
self::useResellerUrl();
}
@@ -28,6 +32,9 @@
*/
public function tearDown(): void
{
+ $this->deleteTestUser('test1@domainscontroller.com');
+ $this->deleteTestDomain('domainscontroller.com');
+
parent::tearDown();
}
@@ -96,6 +103,8 @@
public function testSuspendAndUnsuspend(): void
{
$this->browse(function (Browser $browser) {
+ $sku_domain = \App\Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $user = $this->getTestUser('test1@domainscontroller.com');
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW | Domain::STATUS_ACTIVE
| Domain::STATUS_LDAP_READY | Domain::STATUS_CONFIRMED
@@ -103,6 +112,13 @@
'type' => Domain::TYPE_EXTERNAL,
]);
+ \App\Entitlement::create([
+ 'wallet_id' => $user->wallets()->first()->id,
+ 'sku_id' => $sku_domain->id,
+ 'entitleable_id' => $domain->id,
+ 'entitleable_type' => Domain::class
+ ]);
+
$browser->visit(new DomainPage($domain->id))
->assertVisible('@domain-info #button-suspend')
->assertMissing('@domain-info #button-unsuspend')
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -300,7 +300,7 @@
$expected = ['activesync', 'groupware', 'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage', 'storage'];
- $this->assertUserEntitlements($john, $expected);
+ $this->assertEntitlements($john, $expected);
// Test subscriptions interaction
$browser->with('@general', function (Browser $browser) {
@@ -467,7 +467,7 @@
$alias = UserAlias::where('user_id', $julia->id)->where('alias', 'julia.roberts2@kolab.org')->first();
$this->assertTrue(!empty($alias));
- $this->assertUserEntitlements($julia, ['mailbox', 'storage', 'storage', 'storage', 'storage', 'storage']);
+ $this->assertEntitlements($julia, ['mailbox', 'storage', 'storage', 'storage', 'storage', 'storage']);
$this->assertSame('Julia', $julia->getSetting('first_name'));
$this->assertSame('Roberts', $julia->getSetting('last_name'));
$this->assertSame('Test Org', $julia->getSetting('organization'));
@@ -810,7 +810,7 @@
'storage', 'storage', 'storage', 'storage', 'storage'
];
- $this->assertUserEntitlements($john, $expected);
+ $this->assertEntitlements($john, $expected);
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
@@ -825,7 +825,7 @@
'storage', 'storage', 'storage', 'storage', 'storage'
];
- $this->assertUserEntitlements($john, $expected);
+ $this->assertEntitlements($john, $expected);
});
// TODO: Test that the Distlist SKU is not available for users that aren't a group account owners
diff --git a/src/tests/Feature/Controller/Admin/DomainsTest.php b/src/tests/Feature/Controller/Admin/DomainsTest.php
--- a/src/tests/Feature/Controller/Admin/DomainsTest.php
+++ b/src/tests/Feature/Controller/Admin/DomainsTest.php
@@ -155,6 +155,18 @@
$response->assertStatus(404);
}
+ /**
+ * Test creeating a domain (POST /api/v4/domains)
+ */
+ public function testStore(): void
+ {
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+
+ // Admins can't create domains
+ $response = $this->actingAs($admin)->post("api/v4/domains", []);
+ $response->assertStatus(404);
+ }
+
/**
* Test domain suspending (POST /api/v4/domains/<domain-id>/suspend)
*/
diff --git a/src/tests/Feature/Controller/Admin/SkusTest.php b/src/tests/Feature/Controller/Admin/SkusTest.php
--- a/src/tests/Feature/Controller/Admin/SkusTest.php
+++ b/src/tests/Feature/Controller/Admin/SkusTest.php
@@ -34,6 +34,32 @@
parent::tearDown();
}
+ /**
+ * Test fetching SKUs list for a domain (GET /domains/<id>/skus)
+ */
+ public function testDomainSkus(): void
+ {
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $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);
+
+ // Non-admin access not allowed
+ $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus");
+ $response->assertStatus(403);
+
+ $response = $this->actingAs($admin)->get("api/v4/domains/{$domain->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ // Note: Details are tested where we test API\V4\SkusController
+ }
+
/**
* Test fetching SKUs list
*/
@@ -92,7 +118,7 @@
$json = $response->json();
- $this->assertCount(8, $json);
+ $this->assertCount(6, $json);
// Note: Details are tested where we test API\V4\SkusController
}
}
diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php
--- a/src/tests/Feature/Controller/DomainsTest.php
+++ b/src/tests/Feature/Controller/DomainsTest.php
@@ -7,6 +7,7 @@
use App\Sku;
use App\User;
use App\Wallet;
+use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
@@ -214,6 +215,11 @@
'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,
@@ -247,6 +253,14 @@
$this->assertArrayHasKey('isSuspended', $json);
$this->assertArrayHasKey('isActive', $json);
$this->assertArrayHasKey('isLdapReady', $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');
@@ -318,4 +332,112 @@
// 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 acting as account controller (not owner)
+
+ $this->markTestIncomplete();
+ }
}
diff --git a/src/tests/Feature/Controller/Reseller/DomainsTest.php b/src/tests/Feature/Controller/Reseller/DomainsTest.php
--- a/src/tests/Feature/Controller/Reseller/DomainsTest.php
+++ b/src/tests/Feature/Controller/Reseller/DomainsTest.php
@@ -184,6 +184,18 @@
$response->assertStatus(404);
}
+ /**
+ * Test creeating a domain (POST /api/v4/domains)
+ */
+ public function testStore(): void
+ {
+ $reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
+
+ // Resellers can't create domains
+ $response = $this->actingAs($reseller1)->post("api/v4/domains", []);
+ $response->assertStatus(404);
+ }
+
/**
* Test domain suspending (POST /api/v4/domains/<domain-id>/suspend)
*/
diff --git a/src/tests/Feature/Controller/Reseller/SkusTest.php b/src/tests/Feature/Controller/Reseller/SkusTest.php
--- a/src/tests/Feature/Controller/Reseller/SkusTest.php
+++ b/src/tests/Feature/Controller/Reseller/SkusTest.php
@@ -34,6 +34,43 @@
parent::tearDown();
}
+ /**
+ * Test fetching SKUs list for a domain (GET /domains/<id>/skus)
+ */
+ public function testDomainSkus(): void
+ {
+ $reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
+ $reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $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);
+
+ // User access not allowed
+ $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus");
+ $response->assertStatus(403);
+
+ // Admin access not allowed
+ $response = $this->actingAs($admin)->get("api/v4/domains/{$domain->id}/skus");
+ $response->assertStatus(403);
+
+ // Reseller from another tenant
+ $response = $this->actingAs($reseller2)->get("api/v4/domains/{$domain->id}/skus");
+ $response->assertStatus(404);
+
+ // Reseller access
+ $response = $this->actingAs($reseller1)->get("api/v4/domains/{$domain->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ // Note: Details are tested where we test API\V4\SkusController
+ }
+
/**
* Test fetching SKUs list
*/
@@ -130,7 +167,7 @@
$json = $response->json();
- $this->assertCount(8, $json);
+ $this->assertCount(6, $json);
// Note: Details are tested where we test API\V4\SkusController
}
}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -34,6 +34,46 @@
parent::tearDown();
}
+ /**
+ * Test fetching SKUs list for a domain (GET /domains/<id>/skus)
+ */
+ public function testDomainSkus(): 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' => false,
+ 'readonly' => false,
+ ]);
+ }
/**
* Test fetching SKUs list
@@ -109,7 +149,7 @@
$json = $response->json();
- $this->assertCount(8, $json);
+ $this->assertCount(6, $json);
$this->assertSkuElement('mailbox', $json[0], [
'prio' => 100,
@@ -167,56 +207,31 @@
'required' => ['groupware'],
]);
- $this->assertSkuElement('domain-hosting', $json[6], [
- 'prio' => 0,
- 'type' => 'domain',
- 'handler' => 'domainhosting',
- 'enabled' => false,
- 'readonly' => false,
- ]);
-
- $this->assertSkuElement('group', $json[7], [
- 'prio' => 0,
- 'type' => 'group',
- 'handler' => 'group',
- 'enabled' => false,
- 'readonly' => false,
- ]);
-
- // Test filter by type
- $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus?type=domain");
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertCount(1, $json);
- $this->assertSame('domain', $json[0]['type']);
-
// 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?type=user");
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(8, $json);
- $this->assertSkuElement('meet', $json[5], [
- 'prio' => 50,
+ $this->assertSkuElement('beta', $json[6], [
+ 'prio' => 10,
'type' => 'user',
- 'handler' => 'meet',
+ 'handler' => 'beta',
'enabled' => false,
'readonly' => false,
- 'required' => ['groupware'],
]);
- $this->assertSkuElement('beta', $json[6], [
+ $this->assertSkuElement('distlist', $json[7], [
'prio' => 10,
'type' => 'user',
- 'handler' => 'beta',
+ 'handler' => 'distlist',
'enabled' => false,
'readonly' => false,
+ 'required' => ['beta'],
]);
}
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -655,7 +655,7 @@
$this->assertSame('deleted@kolab.org', $aliases[0]->alias);
$this->assertSame('useralias1@kolab.org', $aliases[1]->alias);
// Assert the new user entitlements
- $this->assertUserEntitlements($user, ['groupware', 'mailbox',
+ $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();
@@ -681,7 +681,7 @@
$this->assertSame('Doe2', $user->getSetting('last_name'));
$this->assertSame('TestOrg', $user->getSetting('organization'));
$this->assertCount(0, $user->aliases()->get());
- $this->assertUserEntitlements($user, ['groupware', 'mailbox',
+ $this->assertEntitlements($user, ['groupware', 'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage']);
// Test acting as account controller (not owner)
@@ -876,7 +876,7 @@
->orderBy('cost')
->pluck('cost')->all();
- $this->assertUserEntitlements(
+ $this->assertEntitlements(
$user,
['groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage']
);
@@ -914,7 +914,7 @@
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(200);
- $this->assertUserEntitlements(
+ $this->assertEntitlements(
$jane,
[
'activesync',
@@ -943,7 +943,7 @@
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(200);
- $this->assertUserEntitlements(
+ $this->assertEntitlements(
$jane,
[
'groupware',
@@ -973,7 +973,7 @@
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(500);
- $this->assertUserEntitlements(
+ $this->assertEntitlements(
$jane,
[
'groupware',
@@ -1003,7 +1003,7 @@
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(500);
- $this->assertUserEntitlements(
+ $this->assertEntitlements(
$jane,
[
'groupware',
@@ -1033,7 +1033,7 @@
$response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
$response->assertStatus(200);
- $this->assertUserEntitlements(
+ $this->assertEntitlements(
$jane,
[
'groupware',
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -92,13 +92,13 @@
/**
* Assert that the entitlements for the user match the expected list of entitlements.
*
- * @param \App\User $user The user for which the entitlements need to be pulled.
- * @param array $expected An array of expected \App\SKU titles.
+ * @param \App\User|\App\Domain $object The object for which the entitlements need to be pulled.
+ * @param array $expected An array of expected \App\SKU titles.
*/
- protected function assertUserEntitlements($user, $expected)
+ protected function assertEntitlements($object, $expected)
{
// Assert the user entitlements
- $skus = $user->entitlements()->get()
+ $skus = $object->entitlements()->get()
->map(function ($ent) {
return $ent->sku->title;
})
diff --git a/src/tests/Unit/Rules/UserEmailDomainTest.php b/src/tests/Unit/Rules/UserEmailDomainTest.php
--- a/src/tests/Unit/Rules/UserEmailDomainTest.php
+++ b/src/tests/Unit/Rules/UserEmailDomainTest.php
@@ -13,6 +13,56 @@
*/
public function testUserEmailDomain(): void
{
- $this->markTestIncomplete();
+ $rules = ['domain' => [new UserEmailDomain()]];
+
+ // Non-string input
+ $v = Validator::make(['domain' => ['domain.tld']], $rules);
+
+ $this->assertTrue($v->fails());
+ $this->assertSame(['domain' => ['The specified domain is invalid.']], $v->errors()->toArray());
+
+ // Non-fqdn name
+ $v = Validator::make(['domain' => 'local'], $rules);
+
+ $this->assertTrue($v->fails());
+ $this->assertSame(['domain' => ['The specified domain is invalid.']], $v->errors()->toArray());
+
+ // www. prefix not allowed
+ $v = Validator::make(['domain' => 'www.local.tld'], $rules);
+
+ $this->assertTrue($v->fails());
+ $this->assertSame(['domain' => ['The specified domain is invalid.']], $v->errors()->toArray());
+
+ // invalid domain
+ $v = Validator::make(['domain' => 'local..tld'], $rules);
+
+ $this->assertTrue($v->fails());
+ $this->assertSame(['domain' => ['The specified domain is invalid.']], $v->errors()->toArray());
+
+ // Valid domain
+ $domain = str_repeat('abcdefghi.', 18) . 'abcdefgh.pl'; // 191 chars
+ $v = Validator::make(['domain' => $domain], $rules);
+
+ $this->assertFalse($v->fails());
+
+ // Domain too long
+ $domain = str_repeat('abcdefghi.', 18) . 'abcdefghi.pl'; // too long domain, 192 chars
+ $v = Validator::make(['domain' => $domain], $rules);
+
+ $this->assertTrue($v->fails());
+ $this->assertSame(['domain' => ['The specified domain is invalid.']], $v->errors()->toArray());
+
+ $rules = ['domain' => [new UserEmailDomain(['kolabnow.com'])]];
+
+ // Domain not belongs to a set of allowed domains
+ $v = Validator::make(['domain' => 'domain.tld'], $rules);
+
+ $this->assertTrue($v->fails());
+ $this->assertSame(['domain' => ['The specified domain is invalid.']], $v->errors()->toArray());
+
+ // Domain on the allowed domains list
+ $v = Validator::make(['domain' => 'kolabNow.com'], $rules);
+
+ $this->assertFalse($v->fails());
}
}
diff --git a/src/tests/Unit/Rules/UserEmailLocalTest.php b/src/tests/Unit/Rules/UserEmailLocalTest.php
--- a/src/tests/Unit/Rules/UserEmailLocalTest.php
+++ b/src/tests/Unit/Rules/UserEmailLocalTest.php
@@ -8,11 +8,51 @@
class UserEmailLocalTest extends TestCase
{
+ /**
+ * List of email address validation cases for testUserEmailLocal()
+ *
+ * @return array Arguments for testUserEmailLocal()
+ */
+ public function dataUserEmailLocal(): array
+ {
+ return [
+ // non-string input
+ [['test'], false, 'The specified user is invalid.'],
+ // Invalid character
+ ['test*test', false, 'The specified user is invalid.'],
+ // Invalid syntax
+ ['test.', false, 'The specified user is invalid.'],
+ // Forbidden names
+ ['Administrator', false, 'The specified user is not available.'],
+ ['Admin', false, 'The specified user is not available.'],
+ ['Sales', false, 'The specified user is not available.'],
+ ['Root', false, 'The specified user is not available.'],
+ // Valid
+ ['test.test', false, null],
+ // Valid for external domains
+ ['Administrator', true, null],
+ ['Admin', true, null],
+ ['Sales', true, null],
+ ['Root', true, null],
+ ];
+ }
+
/**
* Test validation of email local part
+ *
+ * @dataProvider dataUserEmailLocal
*/
- public function testUserEmailLocal(): void
+ public function testUserEmailLocal($user, $external, $error): void
{
- $this->markTestIncomplete();
+ $rules = ['user' => [new UserEmailLocal($external)]];
+
+ $v = Validator::make(['user' => $user], $rules);
+
+ if ($error) {
+ $this->assertTrue($v->fails());
+ $this->assertSame(['user' => [$error]], $v->errors()->toArray());
+ } else {
+ $this->assertFalse($v->fails());
+ }
}
}

File Metadata

Mime Type
text/plain
Expires
Thu, Apr 2, 10:15 PM (1 d, 11 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821188
Default Alt Text
D2894.1775168100.diff (91 KB)

Event Timeline