Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117866928
D1207.1775322477.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
67 KB
Referenced Files
None
Subscribers
None
D1207.1775322477.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D1207: T332120: Status widget improvements
Attached
Detach File
Event Timeline