Page MenuHomePhorge

D1207.1775322477.diff
No OneTemporary

Authored By
Unknown
Size
67 KB
Referenced Files
None
Subscribers
None

D1207.1775322477.diff

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
@@ -4,6 +4,8 @@
use App\Domain;
use App\Http\Controllers\Controller;
+use App\Backends\LDAP;
+use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -57,14 +59,16 @@
}
if (!$domain->confirm()) {
- // TODO: This should include an error message to display to the user
- return response()->json(['status' => 'error']);
+ return response()->json([
+ 'status' => 'error',
+ 'message' => \trans('app.domain-verify-error'),
+ ]);
}
return response()->json([
'status' => 'success',
'statusInfo' => self::statusInfo($domain),
- 'message' => __('app.domain-verify-success'),
+ 'message' => \trans('app.domain-verify-success'),
]);
}
@@ -139,6 +143,56 @@
return response()->json($response);
}
+ /**
+ * Fetch domain status (and reload setup process)
+ *
+ * @param int $id Domain identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function status($id)
+ {
+ $domain = Domain::find($id);
+
+ // Only owner (or admin) has access to the domain
+ if (!Auth::guard()->user()->canRead($domain)) {
+ return $this->errorResponse(403);
+ }
+
+ $response = self::statusInfo($domain);
+
+ if (!empty(request()->input('refresh'))) {
+ $updated = false;
+ $last_step = 'none';
+
+ foreach ($response['process'] as $idx => $step) {
+ $last_step = $step['label'];
+
+ if (!$step['state']) {
+ if (!$this->execProcessStep($domain, $step['label'])) {
+ break;
+ }
+
+ $updated = true;
+ }
+ }
+
+ if ($updated) {
+ $response = self::statusInfo($domain);
+ }
+
+ $success = $response['isReady'];
+ $suffix = $success ? 'success' : 'error-' . $last_step;
+
+ $response['status'] = $success ? 'success' : 'error';
+ $response['message'] = \trans('app.process-' . $suffix);
+ }
+
+ $response = array_merge($response, self::domainStatuses($domain));
+
+ return response()->json($response);
+ }
+
/**
* Update the specified resource in storage.
*
@@ -272,9 +326,56 @@
}
}
+ $state = $count === 0 ? 'done' : 'running';
+
+ // After 180 seconds assume the process is in failed state,
+ // this should unlock the Refresh button in the UI
+ if ($count > 0 && $domain->created_at->diffInSeconds(Carbon::now()) > 180) {
+ $state = 'failed';
+ }
+
return [
'process' => $process,
+ 'processState' => $state,
'isReady' => $count === 0,
];
}
+
+ /**
+ * Execute (synchronously) specified step in a domain setup process.
+ *
+ * @param \App\Domain $domain Domain object
+ * @param string $step Step identifier (as in self::statusInfo())
+ *
+ * @return bool True if the execution succeeded, False otherwise
+ */
+ public static function execProcessStep(Domain $domain, string $step): bool
+ {
+ try {
+ switch ($step) {
+ case 'domain-ldap-ready':
+ // Domain not in LDAP, create it
+ if (!$domain->isLdapReady()) {
+ LDAP::createDomain($domain);
+ $domain->status |= Domain::STATUS_LDAP_READY;
+ $domain->save();
+ }
+ return $domain->isLdapReady();
+
+ case 'domain-verified':
+ // Domain existence not verified
+ $domain->verify();
+ return $domain->isVerified();
+
+ case 'domain-confirmed':
+ // Domain ownership confirmation
+ $domain->confirm();
+ return $domain->isConfirmed();
+ }
+ } catch (\Exception $e) {
+ \Log::error($e);
+ }
+
+ return false;
+ }
}
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
@@ -8,6 +8,7 @@
use App\Rules\UserEmailLocal;
use App\Sku;
use App\User;
+use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@@ -100,6 +101,59 @@
return response()->json($response);
}
+ /**
+ * Fetch user status (and reload setup process)
+ *
+ * @param int $id User identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function status($id)
+ {
+ $user = User::find($id);
+
+ if (empty($user)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canRead($user)) {
+ return $this->errorResponse(403);
+ }
+
+ $response = self::statusInfo($user);
+
+ if (!empty(request()->input('refresh'))) {
+ $updated = false;
+ $last_step = 'none';
+
+ foreach ($response['process'] as $idx => $step) {
+ $last_step = $step['label'];
+
+ if (!$step['state']) {
+ if (!$this->execProcessStep($user, $step['label'])) {
+ break;
+ }
+
+ $updated = true;
+ }
+ }
+
+ if ($updated) {
+ $response = self::statusInfo($user);
+ }
+
+ $success = $response['isReady'];
+ $suffix = $success ? 'success' : 'error-' . $last_step;
+
+ $response['status'] = $success ? 'success' : 'error';
+ $response['message'] = \trans('app.process-' . $suffix);
+ }
+
+ $response = array_merge($response, self::userStatuses($user));
+
+ return response()->json($response);
+ }
+
/**
* User status (extended) information
*
@@ -127,7 +181,6 @@
$process[] = $step;
}
-
list ($local, $domain) = explode('@', $user->email);
$domain = Domain::where('namespace', $domain)->first();
@@ -142,8 +195,17 @@
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 && $user->created_at->diffInSeconds(Carbon::now()) > 180) {
+ $state = 'failed';
+ }
+
return [
'process' => $process,
+ 'processState' => $state,
'isReady' => $all === $checked,
];
}
@@ -489,4 +551,42 @@
$settings = $request->only(array_keys($rules));
unset($settings['password'], $settings['aliases'], $settings['email']);
}
+
+ /**
+ * Execute (synchronously) specified step in a user setup process.
+ *
+ * @param \App\User $user User object
+ * @param string $step Step identifier (as in self::statusInfo())
+ *
+ * @return bool True if the execution succeeded, False otherwise
+ */
+ public static function execProcessStep(User $user, string $step): bool
+ {
+ try {
+ if (strpos($step, 'domain-') === 0) {
+ list ($local, $domain) = explode('@', $user->email);
+ $domain = Domain::where('namespace', $domain)->first();
+
+ return DomainsController::execProcessStep($domain, $step);
+ }
+
+ switch ($step) {
+ case 'user-ldap-ready':
+ // User not in LDAP, create it
+ $job = new \App\Jobs\UserCreate($user);
+ $job->handle();
+ return $user->isLdapReady();
+
+ case 'user-imap-ready':
+ // User not in IMAP? Verify again
+ $job = new \App\Jobs\UserVerify($user);
+ $job->handle();
+ return $user->isImapReady();
+ }
+ } catch (\Exception $e) {
+ \Log::error($e);
+ }
+
+ return false;
+ }
}
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
@@ -12,15 +12,22 @@
'planbutton' => 'Choose :plan',
- 'process-user-new' => 'User registered',
- 'process-user-ldap-ready' => 'User created',
- 'process-user-imap-ready' => 'User mailbox created',
- 'process-domain-new' => 'Custom domain registered',
- 'process-domain-ldap-ready' => 'Custom domain created',
- 'process-domain-verified' => 'Custom domain verified',
- 'process-domain-confirmed' => 'Custom domain ownership verified',
+ 'process-user-new' => 'Registering a user...',
+ 'process-user-ldap-ready' => 'Creating a user...',
+ 'process-user-imap-ready' => 'Creating a mailbox...',
+ 'process-domain-new' => 'Registering a custom domain...',
+ 'process-domain-ldap-ready' => 'Creating a custom domain...',
+ 'process-domain-verified' => 'Verifying a custom domain...',
+ 'process-domain-confirmed' => 'Verifying an ownership of a custom domain...',
+ 'process-success' => 'Setup process finished successfully.',
+ 'process-error-user-ldap-ready' => 'Failed to create a user.',
+ 'process-error-user-imap-ready' => 'Failed to verify that a mailbox exists.',
+ 'process-error-domain-ldap-ready' => 'Failed to create a domain.',
+ 'process-error-domain-verified' => 'Failed to verify a domain.',
+ 'process-error-domain-confirmed' => 'Failed to verify an ownership of a domain.',
'domain-verify-success' => 'Domain verified successfully.',
+ 'domain-verify-error' => 'Domain ownership verification failed.',
'user-update-success' => 'User data updated successfully.',
'user-create-success' => 'User created successfully.',
'user-delete-success' => 'User deleted successfully.',
diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss
--- a/src/resources/sass/app.scss
+++ b/src/resources/sass/app.scss
@@ -137,18 +137,28 @@
}
}
-ul.status-list {
- list-style: none;
- padding: 0;
- margin: 0;
+#status-box {
+ background-color: lighten($green, 35);
- svg {
- width: 1.25rem !important;
- height: 1.25rem;
+ .progress {
+ background-color: #fff;
+ height: 10px;
+ }
+
+ .progress-label {
+ font-size: 0.9em;
}
- span {
- vertical-align: top;
+ .progress-bar {
+ background-color: $green;
+ }
+
+ &.process-failed {
+ background-color: lighten($orange, 30);
+
+ .progress-bar {
+ background-color: $red;
+ }
}
}
@@ -156,15 +166,14 @@
display: flex;
flex-wrap: wrap;
justify-content: center;
- margin-top: 1rem;
& > a {
padding: 1rem;
text-align: center;
white-space: nowrap;
- margin: 0 0.5rem 0.5rem 0;
+ margin-top: 0.5rem;
text-decoration: none;
- min-width: 8rem;
+ width: 150px;
&.disabled {
pointer-events: none;
@@ -176,11 +185,16 @@
top: 0.5rem;
right: 0.5rem;
}
+
+ & + a {
+ margin-left: 0.5rem;
+ }
}
svg {
width: 6rem;
height: 6rem;
+ margin: auto;
}
}
diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue
--- a/src/resources/vue/Dashboard.vue
+++ b/src/resources/vue/Dashboard.vue
@@ -1,23 +1,7 @@
<template>
- <div v-if="!$root.isLoading" class="container" dusk="dashboard-component">
- <div v-if="!isReady" id="status-box" class="card">
- <div class="card-body">
- <div class="card-title">Account status: <span class="text-danger">Not ready</span></div>
- <div class="card-text">
- <p>The process to create your account has not been completed yet.
- Some features may be disabled or readonly.</p>
- <ul class="status-list">
- <li v-for="item in statusProcess" :key="item.label">
- <svg-icon :icon="['far', item.state ? 'check-square' : 'square']"
- :class="item.state ? 'text-success' : 'text-muted'"
- ></svg-icon>
- <router-link v-if="item.link" :to="{ path: item.link }">{{ item.title }}</router-link>
- <span v-if="!item.link">{{ item.title }}</span>
- </li>
- </ul>
- </div>
- </div>
- </div>
+ <div class="container" dusk="dashboard-component">
+ <status-component v-bind:status="status" @status-update="statusUpdate"></status-component>
+
<div id="dashboard-nav">
<router-link class="card link-profile" :to="{ name: 'profile' }">
<svg-icon icon="user-cog"></svg-icon><span class="name">Your profile</span>
@@ -37,12 +21,15 @@
</template>
<script>
+ import StatusComponent from './Widgets/Status'
+
export default {
+ components: {
+ StatusComponent
+ },
data() {
return {
- isReady: true,
- statusProcess: [],
- request: null,
+ status: {},
balance: 0
}
},
@@ -50,14 +37,14 @@
const authInfo = this.$store.state.isLoggedIn ? this.$store.state.authInfo : null
if (authInfo) {
- this.parseStatusInfo(authInfo.statusInfo)
+ this.status = authInfo.statusInfo
this.getBalance(authInfo)
} else {
this.$root.startLoading()
axios.get('/api/auth/info')
.then(response => {
this.$store.state.authInfo = response.data
- this.parseStatusInfo(response.data.statusInfo)
+ this.status = response.data.statusInfo
this.getBalance(response.data)
this.$root.stopLoading()
})
@@ -65,39 +52,16 @@
}
},
methods: {
- // Displays account status information
- parseStatusInfo(info) {
- this.statusProcess = info.process
- this.isReady = info.isReady
-
- // Update status process info every 10 seconds
- // FIXME: This probably should have some limit, or the interval
- // should grow (well, until it could be done with websocket notifications)
- if (!info.isReady && !window.infoRequest) {
- window.infoRequest = setTimeout(() => {
- delete window.infoRequest
- // Stop updates after user logged out
- if (!this.$store.state.isLoggedIn) {
- return;
- }
-
- axios.get('/api/auth/info')
- .then(response => {
- this.$store.state.authInfo = response.data
- this.parseStatusInfo(response.data.statusInfo)
- })
- .catch(error => {
- this.parseStatusInfo(info)
- })
- }, 10000);
- }
- },
getBalance(authInfo) {
this.balance = 0;
// TODO: currencies, multi-wallets, accounts
authInfo.wallets.forEach(wallet => {
this.balance += wallet.balance
})
+ },
+ statusUpdate(user) {
+ this.status = Object.assign({}, this.status, user)
+ this.$store.state.authInfo.statusInfo = this.status
}
}
}
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,22 +1,7 @@
<template>
<div class="container">
- <div v-if="!isReady" id="domain-status-box" class="card">
- <div class="card-body">
- <div class="card-title">Domain status: <span class="text-danger">Not ready</span></div>
- <div class="card-text">
- <p>The process to create the domain has not been completed yet.
- Some features may be disabled or readonly.</p>
- <ul class="status-list">
- <li v-for="item in statusProcess" :key="item.label">
- <svg-icon :icon="['far', item.state ? 'check-square' : 'square']"
- :class="item.state ? 'text-success' : 'text-muted'"
- ></svg-icon>
- <span>{{ item.title }}</span>
- </li>
- </ul>
- </div>
- </div>
- </div>
+ <status-component v-bind:status="status" @status-update="statusUpdate"></status-component>
+
<div v-if="domain && !domain.isConfirmed" class="card" id="domain-verify">
<div class="card-body">
<div class="card-title">Domain verification</div>
@@ -52,14 +37,18 @@
</template>
<script>
+ import StatusComponent from '../Widgets/Status'
+
export default {
+ components: {
+ StatusComponent
+ },
data() {
return {
domain_id: null,
domain: null,
app_name: window.config['app.name'],
- isReady: true,
- statusProcess: []
+ status: {}
}
},
created() {
@@ -70,48 +59,30 @@
if (!this.domain.isConfirmed) {
$('#domain-verify button').focus()
}
- this.parseStatusInfo(response.data.statusInfo)
+ this.status = response.data.statusInfo
})
.catch(this.$root.errorHandler)
} else {
this.$root.errorPage(404)
}
},
- destroyed() {
- clearTimeout(window.domainRequest)
- },
methods: {
confirm() {
axios.get('/api/v4/domains/' + this.domain_id + '/confirm')
.then(response => {
if (response.data.status == 'success') {
this.domain.isConfirmed = true
- this.parseStatusInfo(response.data.statusInfo)
- this.$toast.success(response.data.message)
+ this.status = response.data.statusInfo
}
- })
- },
- // Displays domain status information
- parseStatusInfo(info) {
- this.statusProcess = info.process
- this.isReady = info.isReady
- // Update status process info every 10 seconds
- // FIXME: This probably should have some limit, or the interval
- // should grow (well, until it could be done with websocket notifications)
- if (!info.isReady) {
- window.domainRequest = setTimeout(() => {
- axios.get('/api/v4/domains/' + this.domain_id)
- .then(response => {
- this.domain = response.data
- this.parseStatusInfo(this.domain.statusInfo)
- })
- .catch(error => {
- this.parseStatusInfo(info)
- })
- }, 10000);
- }
+ if (response.data.message) {
+ this.$toast[response.data.status](response.data.message)
+ }
+ })
},
+ statusUpdate(domain) {
+ this.domain = Object.assign({}, this.domain, domain)
+ }
}
}
</script>
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
@@ -1,5 +1,7 @@
<template>
<div class="container">
+ <status-component v-if="user_id !== 'new'" v-bind:status="status" @status-update="statusUpdate"></status-component>
+
<div class="card" id="user-info">
<div class="card-body">
<div class="card-title" v-if="user_id !== 'new'">User account</div>
@@ -148,10 +150,12 @@
<script>
import ListInput from '../Widgets/ListInput'
+ import StatusComponent from '../Widgets/Status'
export default {
components: {
ListInput,
+ StatusComponent
},
data() {
return {
@@ -161,7 +165,8 @@
user: { aliases: [] },
packages: [],
package_id: null,
- skus: []
+ skus: [],
+ status: {}
}
},
created() {
@@ -195,6 +200,7 @@
this.user.last_name = response.data.settings.last_name
this.discount = this.user.wallet.discount
this.discount_description = this.user.wallet.discount_description
+ this.status = response.data.statusInfo
axios.get('/api/v4/skus')
.then(response => {
@@ -345,6 +351,9 @@
}
}
},
+ statusUpdate(user) {
+ this.user = Object.assign({}, this.user, user)
+ }
}
}
</script>
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -97,7 +97,6 @@
return
}
-
// Deleting self, redirect to /profile/delete page
if (id == this.$store.state.authInfo.id) {
this.$router.push({ name: 'profile-delete' })
diff --git a/src/resources/vue/Widgets/Status.vue b/src/resources/vue/Widgets/Status.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/Status.vue
@@ -0,0 +1,177 @@
+<template>
+ <div v-if="!state.isReady" id="status-box" :class="'p-4 mb-3 rounded process-' + className">
+ <div v-if="state.step != 'domain-confirmed'" class="d-flex align-items-start">
+ <p id="status-body" class="flex-grow-1">
+ <span v-if="scope == 'dashboard'">We are preparing your account.</span>
+ <span v-else-if="scope == 'domain'">We are preparing the domain.</span>
+ <span v-else>We are preparing the user account.</span>
+ <br>
+ Some features may be missing or readonly at the moment.<br>
+ <span id="refresh-text" v-if="refresh">The process never ends? Press the "Refresh" button, please.</span>
+ </p>
+ <button v-if="refresh" id="status-refresh" href="#" class="btn btn-secondary" @click="statusRefresh">
+ <svg-icon icon="sync-alt"></svg-icon> Refresh
+ </button>
+ </div>
+ <div v-else class="d-flex align-items-start">
+ <p id="status-body" class="flex-grow-1">
+ <span v-if="scope == 'dashboard'">Your account is almost ready.</span>
+ <span v-else-if="scope == 'domain'">The domain is almost ready.</span>
+ <span v-else>The user account is almost ready.</span>
+ <br>
+ Verify your domain to finish the setup process.
+ </p>
+ <div v-if="scope == 'domain'">
+ <button id="status-verify" class="btn btn-secondary" @click="confirmDomain">
+ <svg-icon icon="sync-alt"></svg-icon> Verify
+ </button>
+ </div>
+ <div v-else-if="state.link && scope != 'domain'">
+ <router-link id="status-link" class="btn btn-secondary" :to="{ path: state.link }">Verify domain</router-link>
+ </div>
+ </div>
+
+ <div class="status-progress text-center">
+ <div class="progress">
+ <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
+ </div>
+ <span class="progress-label">{{ state.title || 'Initializing...' }}</span>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ props: {
+ status: { type: Object, default: {} }
+ },
+ data() {
+ return {
+ className: 'pending',
+ refresh: false,
+ delay: 5000,
+ scope: 'user',
+ state: { isReady: true }
+ }
+ },
+ watch: {
+ // We use property watcher because parent component
+ // might set the property with a delay and we need to parse it
+ status: function (val, oldVal) {
+ this.parseStatusInfo(val)
+ }
+ },
+ destroyed() {
+ clearTimeout(window.infoRequest)
+ },
+ mounted() {
+ this.scope = this.$route.name
+ },
+ methods: {
+ // Displays account status information
+ parseStatusInfo(info) {
+ if (info) {
+ if (!info.isReady) {
+ info.process.forEach((step, idx) => {
+ if (!step.state && !('percent' in info)) {
+ info.title = step.title
+ info.step = step.label
+ info.percent = Math.floor(idx / info.process.length * 100);
+ info.link = step.link
+ }
+ })
+ }
+
+ this.state = info || {}
+
+ this.$nextTick(function() {
+ $(this.$el).find('.progress-bar')
+ .css('width', info.percent + '%')
+ .attr('aria-valuenow', info.percent)
+ })
+
+ // Unhide the Refresh button, the process is in failure state
+ this.refresh = info.processState == 'failed'
+
+ if (this.refresh || info.step == 'domain-confirmed') {
+ this.className = 'failed'
+ }
+ }
+
+ // Update status process info every 10 seconds
+ // FIXME: This probably should have some limit, or the interval
+ // should grow (well, until it could be done with websocket notifications)
+ clearTimeout(window.infoRequest)
+ if (!this.refresh && (!info || !info.isReady)) {
+ window.infoRequest = setTimeout(() => {
+ delete window.infoRequest
+ // Stop updates after user logged out
+ if (!this.$store.state.isLoggedIn) {
+ return;
+ }
+
+ axios.get(this.getUrl())
+ .then(response => {
+ this.parseStatusInfo(response.data)
+ this.emitEvent(response.data)
+ })
+ .catch(error => {
+ this.parseStatusInfo(info)
+ })
+ }, this.delay);
+
+ this.delay += 1000;
+ }
+ },
+ statusRefresh() {
+ clearTimeout(window.infoRequest)
+
+ axios.get(this.getUrl() + '?refresh=1')
+ .then(response => {
+ this.$toast[response.data.status](response.data.message)
+ this.parseStatusInfo(response.data)
+ this.emitEvent(response.data)
+ })
+ .catch(error => {
+ this.parseStatusInfo(this.state)
+ })
+ },
+ confirmDomain() {
+ axios.get('/api/v4/domains/' + this.$route.params.domain + '/confirm')
+ .then(response => {
+ if (response.data.message) {
+ this.$toast[response.data.status](response.data.message)
+ }
+
+ if (response.data.status == 'success') {
+ this.parseStatusInfo(response.data.statusInfo)
+ response.data.isConfirmed = true
+ this.emitEvent(response.data)
+ }
+ })
+ },
+ emitEvent(data) {
+ // Remove useless data and emit the event (to parent components)
+ delete data.status
+ delete data.message
+ this.$emit('status-update', data)
+ },
+ getUrl() {
+ let url
+
+ switch (this.scope) {
+ case 'dashboard':
+ url = '/api/v4/users/' + this.$store.state.authInfo.id + '/status'
+ break
+ case 'domain':
+ url = '/api/v4/domains/' + this.$route.params.domain + '/status'
+ break
+ default:
+ url = '/api/v4/users/' + this.$route.params.user + '/status'
+ }
+
+ return url
+ }
+ }
+ }
+</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -59,11 +59,14 @@
function () {
Route::apiResource('domains', API\V4\DomainsController::class);
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
+ Route::get('domains/{id}/status', 'API\V4\DomainsController@status');
Route::apiResource('entitlements', API\V4\EntitlementsController::class);
Route::apiResource('packages', API\V4\PackagesController::class);
Route::apiResource('skus', API\V4\SkusController::class);
Route::apiResource('users', API\V4\UsersController::class);
+ Route::get('users/{id}/status', 'API\V4\UsersController@status');
+
Route::apiResource('wallets', API\V4\WalletsController::class);
Route::post('payments', 'API\V4\PaymentsController@store');
diff --git a/src/tests/Browser/Components/Status.php b/src/tests/Browser/Components/Status.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Components/Status.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Tests\Browser\Components;
+
+use Laravel\Dusk\Component as BaseComponent;
+
+class Status extends BaseComponent
+{
+ /**
+ * Get the root selector for the component.
+ *
+ * @return string
+ */
+ public function selector()
+ {
+ return '#status-box';
+ }
+
+ /**
+ * Assert that the browser page contains the component.
+ *
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ *
+ * @return void
+ */
+ public function assert($browser)
+ {
+ $browser->waitFor($this->selector());
+ }
+
+ /**
+ * Get the element shortcuts for the component.
+ *
+ * @return array
+ */
+ public function elements()
+ {
+ return [
+ '@body' => "#status-body",
+ '@progress-bar' => ".progress-bar",
+ '@progress-label' => ".progress-label",
+ '@refresh-button' => "#status-refresh",
+ '@refresh-text' => "#refresh-text",
+ ];
+ }
+
+ /**
+ * Assert progress state
+ */
+ public function assertProgress($browser, int $percent, string $label, $class)
+ {
+ $browser->assertVisible('@progress-bar')
+ ->assertAttribute('@progress-bar', 'aria-valuenow', $percent)
+ ->assertSeeIn('@progress-label', $label)
+ ->withinBody(function ($browser) use ($class) {
+ $browser->assertVisible('#status-box.process-' . $class);
+ });
+ }
+}
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
@@ -59,22 +59,23 @@
$browser->visit('/domain/' . $domain->id)
->on(new DomainInfo())
- ->assertVisible('@status')
->whenAvailable('@verify', function ($browser) use ($domain) {
+ $browser->assertSeeIn('pre', $domain->namespace)
+ ->assertSeeIn('pre', $domain->hash())
+ ->click('button')
+ ->assertToast(Toast::TYPE_ERROR, 'Domain ownership verification failed.');
+
// Make sure the domain is confirmed now
- // TODO: Test verification process failure
$domain->status |= Domain::STATUS_CONFIRMED;
$domain->save();
- $browser->assertSeeIn('pre', $domain->namespace)
- ->assertSeeIn('pre', $domain->hash())
- ->click('button');
+ $browser->click('button')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.');
})
->whenAvailable('@config', function ($browser) use ($domain) {
$browser->assertSeeIn('pre', $domain->namespace);
})
- ->assertMissing('@verify')
- ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.');
+ ->assertMissing('@verify');
// Check that confirmed domain page contains only the config box
$browser->visit('/domain/' . $domain->id)
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
@@ -40,7 +40,7 @@
'@app' => '#app',
'@config' => '#domain-config',
'@verify' => '#domain-verify',
- '@status' => '#domain-status-box',
+ '@status' => '#status-box',
];
}
}
diff --git a/src/tests/Browser/Pages/UserInfo.php b/src/tests/Browser/Pages/UserInfo.php
--- a/src/tests/Browser/Pages/UserInfo.php
+++ b/src/tests/Browser/Pages/UserInfo.php
@@ -40,6 +40,7 @@
'@form' => '#user-info form',
'@packages' => '#user-packages',
'@skus' => '#user-skus',
+ '@status' => '#status-box',
];
}
}
diff --git a/src/tests/Browser/StatusTest.php b/src/tests/Browser/StatusTest.php
--- a/src/tests/Browser/StatusTest.php
+++ b/src/tests/Browser/StatusTest.php
@@ -4,7 +4,10 @@
use App\Domain;
use App\User;
+use Carbon\Carbon;
use Tests\Browser;
+use Tests\Browser\Components\Status;
+use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\DomainInfo;
use Tests\Browser\Pages\DomainList;
@@ -23,7 +26,8 @@
{
parent::setUp();
- DB::statement("UPDATE domains SET status = (status | " . Domain::STATUS_CONFIRMED . ")"
+ $domain_status = Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED;
+ DB::statement("UPDATE domains SET status = (status | {$domain_status})"
. " WHERE namespace = 'kolab.org'");
DB::statement("UPDATE users SET status = (status | " . User::STATUS_IMAP_READY . ")"
. " WHERE email = 'john@kolab.org'");
@@ -34,7 +38,8 @@
*/
public function tearDown(): void
{
- DB::statement("UPDATE domains SET status = (status | " . Domain::STATUS_CONFIRMED . ")"
+ $domain_status = Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED;
+ DB::statement("UPDATE domains SET status = (status | {$domain_status})"
. " WHERE namespace = 'kolab.org'");
DB::statement("UPDATE users SET status = (status | " . User::STATUS_IMAP_READY . ")"
. " WHERE email = 'john@kolab.org'");
@@ -47,43 +52,82 @@
*/
public function testDashboard(): void
{
- // Unconfirmed domain
+ // Unconfirmed domain and user
$domain = Domain::where('namespace', 'kolab.org')->first();
$domain->status ^= Domain::STATUS_CONFIRMED;
$domain->save();
+ $john = $this->getTestUser('john@kolab.org');
+ $john->created_at = Carbon::now();
+ $john->status ^= User::STATUS_IMAP_READY;
+ $john->save();
- $this->browse(function ($browser) use ($domain) {
+ $this->browse(function ($browser) use ($john, $domain) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
->on(new Dashboard())
- ->whenAvailable('@status', function ($browser) {
- $browser->assertSeeIn('.card-title', 'Account status:')
- ->assertSeeIn('.card-title span.text-danger', 'Not ready')
- ->with('ul.status-list', function ($browser) {
- $browser->assertElementsCount('li', 7)
- ->assertVisible('li:nth-child(1) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(1) span', 'User registered')
- ->assertVisible('li:nth-child(2) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(2) span', 'User created')
- ->assertVisible('li:nth-child(3) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(3) span', 'User mailbox created')
- ->assertVisible('li:nth-child(4) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(4) span', 'Custom domain registered')
- ->assertVisible('li:nth-child(5) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(5) span', 'Custom domain created')
- ->assertVisible('li:nth-child(6) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(6) span', 'Custom domain verified')
- ->assertVisible('li:nth-child(7) svg.fa-square')
- ->assertSeeIn('li:nth-child(7) a', 'Custom domain ownership verified');
- });
+ ->with(new Status(), function ($browser) use ($john) {
+ $browser->assertSeeIn('@body', 'We are preparing your account')
+ ->assertProgress(28, 'Creating a mailbox...', 'pending')
+ ->assertMissing('#status-verify')
+ ->assertMissing('#status-link')
+ ->assertMissing('@refresh-button')
+ ->assertMissing('@refresh-text');
+
+ $john->created_at = Carbon::now();
+ $john->status ^= User::STATUS_IMAP_READY;
+ $john->save();
+
+ // Wait for auto-refresh, expect domain-confirmed step
+ $browser->pause(6000)
+ ->assertSeeIn('@body', 'Your account is almost ready')
+ ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed')
+ ->assertMissing('@refresh-button')
+ ->assertMissing('@refresh-text')
+ ->assertMissing('#status-verify')
+ ->assertVisible('#status-link');
+ })
+ // check if the link to domain info page works
+ ->click('#status-link')
+ ->on(new DomainInfo())
+ ->back()
+ ->on(new Dashboard())
+ ->with(new Status(), function ($browser) use ($john) {
+ $browser->assertMissing('@refresh-button')
+ ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed');
});
// Confirm the domain and wait until the whole status box disappears
$domain->status |= Domain::STATUS_CONFIRMED;
$domain->save();
- // At the moment, this may take about 10 seconds
- $browser->waitUntilMissing('@status', 15);
+ // This should take less than 10 seconds
+ $browser->waitUntilMissing('@status', 10);
+ });
+
+ // Test the Refresh button
+ $domain->status ^= Domain::STATUS_CONFIRMED;
+ $domain->save();
+ $john->created_at = Carbon::now()->subSeconds(3600);
+ $john->status ^= User::STATUS_IMAP_READY;
+ $john->save();
+
+ $this->browse(function ($browser) use ($john, $domain) {
+ $browser->visit(new Dashboard())
+ ->with(new Status(), function ($browser) use ($john, $domain) {
+ $browser->assertSeeIn('@body', 'We are preparing your account')
+ ->assertProgress(28, 'Creating a mailbox...', 'failed')
+ ->assertVisible('@refresh-button')
+ ->assertVisible('@refresh-text');
+
+ $john->status ^= User::STATUS_IMAP_READY;
+ $john->save();
+ $domain->status |= Domain::STATUS_CONFIRMED;
+ $domain->save();
+
+ $browser->click('@refresh-button')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Setup process finished successfully.');
+ })
+ ->assertMissing('@status');
});
}
@@ -95,10 +139,12 @@
public function testDomainStatus(): void
{
$domain = Domain::where('namespace', 'kolab.org')->first();
- $domain->status ^= Domain::STATUS_CONFIRMED;
+ $domain->created_at = Carbon::now();
+ $domain->status ^= Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED;
$domain->save();
$this->browse(function ($browser) use ($domain) {
+ // Test auto-refresh
$browser->on(new Dashboard())
->click('@links a.link-domains')
->on(new DomainList())
@@ -107,43 +153,58 @@
->assertText('@table tbody tr:first-child td:first-child svg title', 'Not Ready')
->click('@table tbody tr:first-child td:first-child a')
->on(new DomainInfo())
- ->whenAvailable('@status', function ($browser) {
- $browser->assertSeeIn('.card-title', 'Domain status:')
- ->assertSeeIn('.card-title span.text-danger', 'Not ready')
- ->with('ul.status-list', function ($browser) {
- $browser->assertElementsCount('li', 4)
- ->assertVisible('li:nth-child(1) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(1) span', 'Custom domain registered')
- ->assertVisible('li:nth-child(2) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(2) span', 'Custom domain created')
- ->assertVisible('li:nth-child(3) svg.fa-check-square')
- ->assertSeeIn('li:nth-child(3) span', 'Custom domain verified')
- ->assertVisible('li:nth-child(4) svg.fa-square')
- ->assertSeeIn('li:nth-child(4) span', 'Custom domain ownership verified');
- });
+ ->with(new Status(), function ($browser) {
+ $browser->assertSeeIn('@body', 'We are preparing the domain')
+ ->assertProgress(50, 'Verifying a custom domain...', 'pending')
+ ->assertMissing('@refresh-button')
+ ->assertMissing('@refresh-text')
+ ->assertMissing('#status-link')
+ ->assertMissing('#status-verify');
+ });
+
+ $domain->status |= Domain::STATUS_VERIFIED;
+ $domain->save();
+
+ // This should take less than 10 seconds
+ $browser->waitFor('@status.process-failed')
+ ->with(new Status(), function ($browser) {
+ $browser->assertSeeIn('@body', 'The domain is almost ready')
+ ->assertProgress(75, 'Verifying an ownership of a custom domain...', 'failed')
+ ->assertMissing('@refresh-button')
+ ->assertMissing('@refresh-text')
+ ->assertMissing('#status-link')
+ ->assertVisible('#status-verify');
});
- // Confirm the domain and wait until the whole status box disappears
$domain->status |= Domain::STATUS_CONFIRMED;
$domain->save();
- // At the moment, this may take about 10 seconds
- $browser->waitUntilMissing('@status', 15);
+ // Test Verify button
+ $browser->click('@status #status-verify')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.')
+ ->waitUntilMissing('@status')
+ ->assertMissing('@verify')
+ ->assertVisible('@config');
});
}
/**
- * Test user status on users list
+ * Test user status on users list and user info page
*
* @depends testDashboard
*/
public function testUserStatus(): void
{
$john = $this->getTestUser('john@kolab.org');
+ $john->created_at = Carbon::now();
$john->status ^= User::STATUS_IMAP_READY;
$john->save();
- $this->browse(function ($browser) {
+ $domain = Domain::where('namespace', 'kolab.org')->first();
+ $domain->status ^= Domain::STATUS_CONFIRMED;
+ $domain->save();
+
+ $this->browse(function ($browser) use ($john, $domain) {
$browser->visit(new Dashboard())
->click('@links a.link-users')
->on(new UserList())
@@ -155,13 +216,38 @@
->click('@table tbody tr:nth-child(3) td:first-child a')
->on(new UserInfo())
->with('@form', function (Browser $browser) {
- // Assert stet in the user edit form
+ // Assert state in the user edit form
$browser->assertSeeIn('div.row:nth-child(1) label', 'Status')
->assertSeeIn('div.row:nth-child(1) #status', 'Not Ready');
- });
+ })
+ ->with(new Status(), function ($browser) use ($john) {
+ $browser->assertSeeIn('@body', 'We are preparing the user account')
+ ->assertProgress(28, 'Creating a mailbox...', 'pending')
+ ->assertMissing('#status-verify')
+ ->assertMissing('#status-link')
+ ->assertMissing('@refresh-button')
+ ->assertMissing('@refresh-text');
+
+ $john->status ^= User::STATUS_IMAP_READY;
+ $john->save();
+
+ // Wait for auto-refresh, expect domain-confirmed step
+ $browser->pause(6000)
+ ->assertSeeIn('@body', 'The user account is almost ready')
+ ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed')
+ ->assertMissing('@refresh-button')
+ ->assertMissing('@refresh-text')
+ ->assertMissing('#status-verify')
+ ->assertVisible('#status-link');
+ })
+ ->assertSeeIn('#status', 'Active');
+
+ // Confirm the domain and wait until the whole status box disappears
+ $domain->status |= Domain::STATUS_CONFIRMED;
+ $domain->save();
- // TODO: The status should also be live-updated here
- // Maybe when we have proper websocket communication
+ // This should take less than 10 seconds
+ $browser->waitUntilMissing('@status', 10);
});
}
}
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
@@ -58,7 +58,9 @@
$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();
@@ -186,4 +188,56 @@
$response = $this->actingAs($jack)->get("api/v4/domains/{$domain->id}");
$response->assertStatus(403);
}
+
+ /**
+ * 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_VERIFIED | Domain::STATUS_CONFIRMED;
+ $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->assertFalse($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(false, $json['process'][3]['state']);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('Failed to verify an ownership of a domain.', $json['message']);
+
+ // TODO: Test completing all process steps
+ }
}
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
@@ -9,6 +9,7 @@
use App\Sku;
use App\User;
use App\Wallet;
+use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
@@ -33,6 +34,8 @@
$wallet = $user->wallets()->first();
$wallet->discount()->dissociate();
$wallet->save();
+ $user->status |= User::STATUS_IMAP_READY;
+ $user->save();
}
/**
@@ -51,6 +54,8 @@
$wallet = $user->wallets()->first();
$wallet->discount()->dissociate();
$wallet->save();
+ $user->status |= User::STATUS_IMAP_READY;
+ $user->save();
parent::tearDown();
}
@@ -209,125 +214,6 @@
$this->assertSame($ned->email, $json[3]['email']);
}
- public function testStatusInfo(): void
- {
- $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
- $domain = $this->getTestDomain('userscontroller.com', [
- 'status' => Domain::STATUS_NEW,
- 'type' => Domain::TYPE_PUBLIC,
- ]);
-
- $user->status = User::STATUS_NEW;
- $user->save();
-
- $result = UsersController::statusInfo($user);
-
- $this->assertFalse($result['isReady']);
- $this->assertCount(3, $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']);
- $this->assertSame('user-imap-ready', $result['process'][2]['label']);
- $this->assertSame(false, $result['process'][2]['state']);
-
- $user->status |= User::STATUS_LDAP_READY | User::STATUS_IMAP_READY;
- $user->save();
-
- $result = UsersController::statusInfo($user);
-
- $this->assertTrue($result['isReady']);
- $this->assertCount(3, $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']);
-
- $domain->status |= Domain::STATUS_VERIFIED;
- $domain->type = Domain::TYPE_EXTERNAL;
- $domain->save();
-
- $result = UsersController::statusInfo($user);
-
- $this->assertFalse($result['isReady']);
- $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']);
- }
-
- /**
- * Test user data response used in show and info actions
- */
- public function testUserResponse(): void
- {
- $user = $this->getTestUser('john@kolab.org');
- $wallet = $user->wallets()->first();
- $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['aliases']));
- $this->assertCount(1, $result['aliases']);
- $this->assertSame('john.doe@kolab.org', $result['aliases'][0]);
-
- $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']);
-
- $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']);
-
- // Test discount in a response
- $discount = Discount::where('code', 'TEST')->first();
- $wallet->discount()->associate($discount);
- $wallet->save();
- $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($discount->id, $result['wallets'][0]['discount_id']);
- $this->assertSame($discount->discount, $result['wallets'][0]['discount']);
- $this->assertSame($discount->description, $result['wallets'][0]['discount_description']);
- }
/**
* Test fetching user data/profile (GET /api/v4/users/<user-id>)
@@ -391,6 +277,130 @@
$this->assertSame(1, $json['skus'][$secondfactor_sku->id]['count']);
}
+ /**
+ * 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
+ {
+ $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);
+
+ $john->status ^= User::STATUS_IMAP_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']);
+ $this->assertCount(7, $json['process']);
+ $this->assertSame('user-imap-ready', $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 user in imap syncronously
+ $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status?refresh=1");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertTrue($json['isImapReady']);
+ $this->assertFalse($json['isReady']);
+ $this->assertCount(7, $json['process']);
+ $this->assertSame('user-imap-ready', $json['process'][2]['label']);
+ $this->assertSame(true, $json['process'][2]['state']);
+ $this->assertSame('domain-confirmed', $json['process'][6]['label']);
+ $this->assertSame(false, $json['process'][6]['state']);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('Failed to verify an ownership of a domain.', $json['message']);
+
+ // TODO: Test completing all process steps
+ }
+
+ /**
+ * 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->assertCount(3, $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']);
+ $this->assertSame('user-imap-ready', $result['process'][2]['label']);
+ $this->assertSame(false, $result['process'][2]['state']);
+ $this->assertSame('running', $result['processState']);
+
+ $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']);
+ $this->assertCount(3, $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('done', $result['processState']);
+
+ $domain->status |= Domain::STATUS_VERIFIED;
+ $domain->type = Domain::TYPE_EXTERNAL;
+ $domain->save();
+
+ $result = UsersController::statusInfo($user);
+
+ $this->assertFalse($result['isReady']);
+ $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']);
+ }
/**
* Test user creation (POST /api/v4/users)
*/
@@ -710,6 +720,66 @@
$this->markTestIncomplete();
}
+ /**
+ * Test user data response used in show and info actions
+ */
+ public function testUserResponse(): void
+ {
+ $user = $this->getTestUser('john@kolab.org');
+ $wallet = $user->wallets()->first();
+ $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['aliases']));
+ $this->assertCount(1, $result['aliases']);
+ $this->assertSame('john.doe@kolab.org', $result['aliases'][0]);
+
+ $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']);
+
+ $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']);
+
+ // Test discount in a response
+ $discount = Discount::where('code', 'TEST')->first();
+ $wallet->discount()->associate($discount);
+ $wallet->save();
+ $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($discount->id, $result['wallets'][0]['discount_id']);
+ $this->assertSame($discount->discount, $result['wallets'][0]['discount']);
+ $this->assertSame($discount->description, $result['wallets'][0]['discount_description']);
+ }
+
/**
* List of alias validation cases for testValidateEmail()
*

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 5:07 PM (6 h, 24 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18806448
Default Alt Text
D1207.1775322477.diff (67 KB)

Event Timeline