Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117790235
D2906.1775256017.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
25 KB
Referenced Files
None
Subscribers
None
D2906.1775256017.diff
View Options
diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -381,6 +381,31 @@
}
/**
+ * Checks if there are any objects (users/aliases/groups) in a domain.
+ * Note: Public domains are always reported not empty.
+ *
+ * @return bool True if there are no objects assigned, False otherwise
+ */
+ public function isEmpty(): bool
+ {
+ if ($this->isPublic()) {
+ return false;
+ }
+
+ // FIXME: These queries will not use indexes, so maybe we should consider
+ // wallet/entitlements to search in objects that belong to this domain account?
+
+ $suffix = '@' . $this->namespace;
+ $suffixLen = strlen($suffix);
+
+ return !(
+ \App\User::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists()
+ || \App\UserAlias::whereRaw('substr(alias, ?) = ?', [-$suffixLen, $suffix])->exists()
+ || \App\Group::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists()
+ );
+ }
+
+ /**
* Any (additional) properties of this domain.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
@@ -436,8 +461,9 @@
/**
* List the users of a domain, so long as the domain is not a public registration domain.
+ * Note: It returns only users with a mailbox.
*
- * @return array
+ * @return \App\User[] A list of users
*/
public function users(): array
{
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -9,6 +9,18 @@
class DomainsController extends \App\Http\Controllers\API\V4\DomainsController
{
/**
+ * Remove the specified domain.
+ *
+ * @param int $id Domain identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function destroy($id)
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
* Search for domains
*
* @return \Illuminate\Http\JsonResponse
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
@@ -39,7 +39,7 @@
}
/**
- * Show the form for creating a new resource.
+ * Show the form for creating a new domain.
*
* @return \Illuminate\Http\JsonResponse
*/
@@ -82,21 +82,42 @@
}
/**
- * Remove the specified resource from storage.
+ * Remove the specified domain.
*
- * @param int $id
+ * @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
- return $this->errorResponse(404);
+ $domain = Domain::withEnvTenantContext()->find($id);
+
+ if (empty($domain)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canDelete($domain)) {
+ return $this->errorResponse(403);
+ }
+
+ // It is possible to delete domain only if there are no users/aliases/groups using it.
+ if (!$domain->isEmpty()) {
+ $response = ['status' => 'error', 'message' => \trans('app.domain-notempty-error')];
+ return response()->json($response, 422);
+ }
+
+ $domain->delete();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans('app.domain-delete-success'),
+ ]);
}
/**
- * Show the form for editing the specified resource.
+ * Show the form for editing the specified domain.
*
- * @param int $id
+ * @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
@@ -168,9 +189,15 @@
$namespace = \strtolower(request()->input('namespace'));
// Domain already exists
- if (Domain::withTrashed()->where('namespace', $namespace)->exists()) {
- $errors = ['namespace' => \trans('validation.domainnotavailable')];
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ if ($domain = Domain::withTrashed()->where('namespace', $namespace)->first()) {
+ // Check if the domain is soft-deleted and belongs to the same user
+ $deleteBeforeCreate = $domain->trashed() && ($wallet = $domain->wallet())
+ && $wallet->owner && $wallet->owner->id == $owner->id;
+
+ if (!$deleteBeforeCreate) {
+ $errors = ['namespace' => \trans('validation.domainnotavailable')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
}
if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) {
@@ -185,7 +212,10 @@
DB::beginTransaction();
- // TODO: Force-delete domain if it is soft-deleted and belongs to the same user
+ // Force-delete the existing domain if it is soft-deleted and belongs to the same user
+ if (!empty($deleteBeforeCreate)) {
+ $domain->forceDelete();
+ }
// Create the domain
$domain = Domain::create([
@@ -309,10 +339,10 @@
}
/**
- * Update the specified resource in storage.
+ * Update the specified domain.
*
* @param \Illuminate\Http\Request $request
- * @param int $id
+ * @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -452,9 +452,6 @@
return response
},
error => {
- let error_msg
- let status = error.response ? error.response.status : 200
-
// Do not display the error in a toast message, pass the error as-is
if (error.config.ignoreErrors) {
return Promise.reject(error)
@@ -464,15 +461,20 @@
error.config.onFinish()
}
- if (error.response && status == 422) {
- error_msg = "Form validation error"
+ let error_msg
+
+ const status = error.response ? error.response.status : 200
+ const data = error.response ? error.response.data : {}
+
+ if (status == 422 && data.errors) {
+ error_msg = app.$t('error.form')
const modal = $('div.modal.show')
$(modal.length ? modal : 'form').each((i, form) => {
form = $(form)
- $.each(error.response.data.errors || {}, (idx, msg) => {
+ $.each(data.errors, (idx, msg) => {
const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx
let input = form.find('#' + input_name)
@@ -526,8 +528,8 @@
form.find('.is-invalid:not(.listinput-widget)').first().focus()
})
}
- else if (error.response && error.response.data) {
- error_msg = error.response.data.message
+ else if (data.status == 'error') {
+ error_msg = data.message
}
else {
error_msg = error.request ? error.request.statusText : error.message
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
@@ -51,6 +51,8 @@
'distlist-unsuspend-success' => 'Distribution list unsuspended successfully.',
'domain-create-success' => 'Domain created successfully.',
+ 'domain-delete-success' => 'Domain deleted successfully.',
+ 'domain-notempty-error' => 'Unable to delete a domain with assigned users or other objects.',
'domain-verify-success' => 'Domain verified successfully.',
'domain-verify-error' => 'Domain ownership verification failed.',
'domain-suspend-success' => 'Domain suspended successfully.',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -61,6 +61,11 @@
],
'domain' => [
+ 'delete' => "Delete domain",
+ 'delete-domain' => "Delete {domain}",
+ 'delete-text' => "Do you really want to delete this domain permanently?"
+ . " This is only possible if there are no users, aliases or other objects in this domain."
+ . " Please note that this action cannot be undone.",
'dns-verify' => "Domain DNS verification sample:",
'dns-config' => "Domain DNS configuration sample:",
'namespace' => "Namespace",
@@ -92,6 +97,7 @@
'500' => "Internal server error",
'unknown' => "Unknown Error",
'server' => "Server Error",
+ 'form' => "Form validation error",
],
'form' => [
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
@@ -5,7 +5,14 @@
<div class="card">
<div class="card-body">
<div class="card-title" v-if="domain_id === 'new'">{{ $t('domain.new') }}</div>
- <div class="card-title" v-else>{{ $t('form.domain') }}</div>
+ <div class="card-title" v-else>{{ $t('form.domain') }}
+ <button
+ class="btn btn-outline-danger button-delete float-end"
+ @click="showDeleteConfirmation()" type="button"
+ >
+ <svg-icon icon="trash-alt"></svg-icon> {{ $t('domain.delete') }}
+ </button>
+ </div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
@@ -93,10 +100,30 @@
</div>
</div>
</div>
+ <div id="delete-warning" class="modal" tabindex="-1" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">{{ $t('domain.delete-domain', { domain: domain.namespace }) }}</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
+ </div>
+ <div class="modal-body">
+ <p>{{ $t('domain.delete-text') }}</p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-danger modal-action" @click="deleteDomain()">
+ <svg-icon icon="trash-alt"></svg-icon> {{ $t('btn.delete') }}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</template>
<script>
+ import { Modal } from 'bootstrap'
import ListInput from '../Widgets/ListInput'
import PackageSelect from '../Widgets/PackageSelect'
import StatusComponent from '../Widgets/Status'
@@ -140,6 +167,9 @@
},
mounted() {
$('#namespace').focus()
+ $('#delete-warning')[0].addEventListener('shown.bs.modal', event => {
+ $(event.target).find('button.modal-cancel').focus()
+ })
},
methods: {
confirm() {
@@ -155,6 +185,20 @@
}
})
},
+ deleteDomain() {
+ // Delete the domain from the confirm dialog
+ axios.delete('/api/v4/domains/' + this.domain_id)
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$toast.success(response.data.message)
+ this.$router.push({ name: 'domains' })
+ }
+ })
+ },
+ showDeleteConfirmation() {
+ // Display the warning
+ new Modal('#delete-warning').show()
+ },
statusUpdate(domain) {
this.domain = Object.assign({}, this.domain, domain)
},
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
@@ -5,6 +5,7 @@
use App\Domain;
use App\User;
use Tests\Browser;
+use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\ListInput;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
@@ -295,4 +296,58 @@
->assertSeeIn('@table tr:nth-child(2) a', 'testdomain.com');
});
}
+
+ /**
+ * Test domain deletion
+ */
+ public function testDomainDelete(): void
+ {
+ // Create the domain to delete
+ $john = $this->getTestUser('john@kolab.org');
+ $domain = $this->getTestDomain('testdomain.com', ['type' => Domain::TYPE_EXTERNAL]);
+ $packageDomain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $domain->assignPackage($packageDomain, $john);
+
+ $this->browse(function ($browser) {
+ $browser->visit('/login')
+ ->on(new Home())
+ ->submitLogon('john@kolab.org', 'simple123')
+ ->visit('/domains')
+ ->on(new DomainList())
+ ->assertElementsCount('@table tbody tr', 2)
+ ->assertSeeIn('@table tr:nth-child(2) a', 'testdomain.com')
+ ->click('@table tbody tr:nth-child(2) a')
+ ->on(new DomainInfo())
+ ->waitFor('button.button-delete')
+ ->assertSeeIn('button.button-delete', 'Delete domain')
+ ->click('button.button-delete')
+ ->with(new Dialog('#delete-warning'), function ($browser) {
+ $browser->assertSeeIn('@title', 'Delete testdomain.com')
+ ->assertFocused('@button-cancel')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Delete')
+ ->click('@button-cancel');
+ })
+ ->waitUntilMissing('#delete-warning')
+ ->click('button.button-delete')
+ ->with(new Dialog('#delete-warning'), function (Browser $browser) {
+ $browser->click('@button-action');
+ })
+ ->waitUntilMissing('#delete-warning')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Domain deleted successfully.')
+ ->on(new DomainList())
+ ->assertElementsCount('@table tbody tr', 1);
+
+ // Test error handling on deleting a non-empty domain
+ $err = 'Unable to delete a domain with assigned users or other objects.';
+ $browser->click('@table tbody tr:nth-child(1) a')
+ ->on(new DomainInfo())
+ ->waitFor('button.button-delete')
+ ->click('button.button-delete')
+ ->with(new Dialog('#delete-warning'), function ($browser) {
+ $browser->click('@button-action');
+ })
+ ->assertToast(Toast::TYPE_ERROR, $err);
+ });
+ }
}
diff --git a/src/tests/Feature/Controller/Admin/DomainsTest.php b/src/tests/Feature/Controller/Admin/DomainsTest.php
--- a/src/tests/Feature/Controller/Admin/DomainsTest.php
+++ b/src/tests/Feature/Controller/Admin/DomainsTest.php
@@ -26,6 +26,7 @@
*/
public function tearDown(): void
{
+ $this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
parent::tearDown();
@@ -45,6 +46,19 @@
}
/**
+ * Test deleting a domain (DELETE /api/v4/domains/<id>)
+ */
+ public function testDestroy(): void
+ {
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $domain = $this->getTestDomain('kolab.org');
+
+ // This end-point does not exist for admins
+ $response = $this->actingAs($admin)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(404);
+ }
+
+ /**
* Test domains searching (/api/v4/domains)
*/
public function testIndex(): void
@@ -178,7 +192,7 @@
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
- $user = $this->getTestUser('test@domainscontroller.com');
+ $user = $this->getTestUser('test1@domainscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Test unauthorized access to admin API
@@ -211,7 +225,7 @@
'status' => Domain::STATUS_NEW | Domain::STATUS_SUSPENDED,
'type' => Domain::TYPE_EXTERNAL,
]);
- $user = $this->getTestUser('test@domainscontroller.com');
+ $user = $this->getTestUser('test1@domainscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Test unauthorized access to admin API
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
@@ -20,12 +20,16 @@
{
parent::setUp();
+ $this->deleteTestUser('test1@' . \config('app.domain'));
+ $this->deleteTestUser('test2@' . \config('app.domain'));
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
}
public function tearDown(): void
{
+ $this->deleteTestUser('test1@' . \config('app.domain'));
+ $this->deleteTestUser('test2@' . \config('app.domain'));
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
@@ -40,6 +44,8 @@
*/
public function testConfirm(): void
{
+ Queue::fake();
+
$sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
@@ -88,6 +94,81 @@
}
/**
+ * Test domain delete request (DELETE /api/v4/domains/<id>)
+ */
+ public function testDestroy(): void
+ {
+ Queue::fake();
+
+ $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $john = $this->getTestUser('john@kolab.org');
+ $johns_domain = $this->getTestDomain('kolab.org');
+ $user1 = $this->getTestUser('test1@' . \config('app.domain'));
+ $user2 = $this->getTestUser('test2@' . \config('app.domain'));
+ $domain = $this->getTestDomain('domainscontroller.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_EXTERNAL,
+ ]);
+
+ Entitlement::create([
+ 'wallet_id' => $user1->wallets()->first()->id,
+ 'sku_id' => $sku_domain->id,
+ 'entitleable_id' => $domain->id,
+ 'entitleable_type' => Domain::class
+ ]);
+
+ // Not authorized access
+ $response = $this->actingAs($john)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(403);
+
+ // Can't delete non-empty domain
+ $response = $this->actingAs($john)->delete("api/v4/domains/{$johns_domain->id}");
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertEquals('error', $json['status']);
+ $this->assertEquals('Unable to delete a domain with assigned users or other objects.', $json['message']);
+
+ // Successful deletion
+ $response = $this->actingAs($user1)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertEquals('success', $json['status']);
+ $this->assertEquals('Domain deleted successfully.', $json['message']);
+ $this->assertTrue($domain->fresh()->trashed());
+
+ // Authorized access by additional account controller
+ $this->deleteTestDomain('domainscontroller.com');
+ $domain = $this->getTestDomain('domainscontroller.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_EXTERNAL,
+ ]);
+
+ Entitlement::create([
+ 'wallet_id' => $user1->wallets()->first()->id,
+ 'sku_id' => $sku_domain->id,
+ 'entitleable_id' => $domain->id,
+ 'entitleable_type' => Domain::class
+ ]);
+
+ $user1->wallets()->first()->addController($user2);
+
+ $response = $this->actingAs($user2)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $this->assertCount(2, $json);
+ $this->assertEquals('success', $json['status']);
+ $this->assertEquals('Domain deleted successfully.', $json['message']);
+ $this->assertTrue($domain->fresh()->trashed());
+ }
+
+ /**
* Test fetching domains list
*/
public function testIndex(): void
@@ -436,6 +517,37 @@
$wallet = $domain->wallet();
$this->assertSame($john->wallets->first()->id, $wallet->id);
+ // Test re-creating a domain
+ $domain->delete();
+
+ $response = $this->actingAs($john)->post("/api/v4/domains", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Domain created successfully.", $json['message']);
+ $this->assertCount(2, $json);
+
+ $domain = Domain::where('namespace', $post['namespace'])->first();
+ $this->assertInstanceOf(Domain::class, $domain);
+ $this->assertEntitlements($domain, ['domain-hosting']);
+ $wallet = $domain->wallet();
+ $this->assertSame($john->wallets->first()->id, $wallet->id);
+
+ // Test creating a domain that is soft-deleted and belongs to another user
+ $domain->delete();
+ $domain->entitlement()->withTrashed()->update(['wallet_id' => $jack->wallets->first()->id]);
+
+ $response = $this->actingAs($john)->post("/api/v4/domains", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json);
+ $this->assertSame('The specified domain is not available.', $json['errors']['namespace']);
+
// Test acting as account controller (not owner)
$this->markTestIncomplete();
diff --git a/src/tests/Feature/Controller/Reseller/DomainsTest.php b/src/tests/Feature/Controller/Reseller/DomainsTest.php
--- a/src/tests/Feature/Controller/Reseller/DomainsTest.php
+++ b/src/tests/Feature/Controller/Reseller/DomainsTest.php
@@ -27,6 +27,7 @@
*/
public function tearDown(): void
{
+ $this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
parent::tearDown();
@@ -49,6 +50,19 @@
}
/**
+ * Test deleting a domain (DELETE /api/v4/domains/<id>)
+ */
+ public function testDestroy(): void
+ {
+ $reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
+ $domain = $this->getTestDomain('kolab.org');
+
+ // This end-point does not exist for resellers
+ $response = $this->actingAs($reseller1)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(404);
+ }
+
+ /**
* Test domains searching (/api/v4/domains)
*/
public function testIndex(): void
@@ -213,7 +227,7 @@
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
- $user = $this->getTestUser('test@domainscontroller.com');
+ $user = $this->getTestUser('test1@domainscontroller.com');
// Test unauthorized access to the reseller API (user)
$response = $this->actingAs($user)->post("/api/v4/domains/{$domain->id}/suspend", []);
@@ -263,7 +277,7 @@
'status' => Domain::STATUS_NEW | Domain::STATUS_SUSPENDED,
'type' => Domain::TYPE_EXTERNAL,
]);
- $user = $this->getTestUser('test@domainscontroller.com');
+ $user = $this->getTestUser('test1@domainscontroller.com');
// Test unauthorized access to reseller API (user)
$response = $this->actingAs($user)->post("/api/v4/domains/{$domain->id}/unsuspend", []);
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -230,6 +230,34 @@
}
/**
+ * Test isEmpty() method
+ */
+ public function testIsEmpty(): void
+ {
+ Queue::fake();
+
+ // Empty domain
+ $domain = $this->getTestDomain('gmail.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_EXTERNAL,
+ ]);
+
+ $this->assertTrue($domain->isEmpty());
+
+ // TODO: Test with adding a group/alias/user, each separately
+
+ // Empty public domain
+ $domain = Domain::where('namespace', 'libertymail.net')->first();
+
+ $this->assertFalse($domain->isEmpty());
+
+ // Non-empty private domain
+ $domain = Domain::where('namespace', 'kolab.org')->first();
+
+ $this->assertFalse($domain->isEmpty());
+ }
+
+ /**
* Test domain restoring
*/
public function testRestore(): void
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 10:40 PM (53 m, 6 s ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18827122
Default Alt Text
D2906.1775256017.diff (25 KB)
Attached To
Mode
D2906: Domain deletion UI
Attached
Detach File
Event Timeline