-
+
{{ $root.userStatusText(user) }}
diff --git a/src/resources/vue/Widgets/ListInput.vue b/src/resources/vue/Widgets/ListInput.vue
--- a/src/resources/vue/Widgets/ListInput.vue
+++ b/src/resources/vue/Widgets/ListInput.vue
@@ -46,16 +46,22 @@
if (value) {
this.list.push(value)
this.input.value = ''
+ this.input.classList.remove('is-invalid')
+
if (focus !== false) {
this.input.focus()
}
+
+ if (this.list.length == 1) {
+ this.$el.classList.remove('is-invalid')
+ }
}
},
deleteItem(index) {
this.$delete(this.list, index)
- if (this.list.length == 1) {
- $(this.$el).removeClass('is-invalid')
+ if (!this.list.length) {
+ this.$el.classList.remove('is-invalid')
}
},
keyDown(e) {
diff --git a/src/resources/vue/Widgets/Status.vue b/src/resources/vue/Widgets/Status.vue
--- a/src/resources/vue/Widgets/Status.vue
+++ b/src/resources/vue/Widgets/Status.vue
@@ -4,6 +4,7 @@
We are preparing your account.
We are preparing the domain.
+ We are preparing the distribution list.
We are preparing the user account.
Some features may be missing or readonly at the moment.
@@ -17,6 +18,7 @@
Your account is almost ready.
The domain is almost ready.
+ The distribution list is almost ready.
The user account is almost ready.
Verify your domain to finish the setup process.
@@ -187,6 +189,9 @@
case 'domain':
url = '/api/v4/domains/' + this.$route.params.domain + '/status'
break
+ case 'distlist':
+ url = '/api/v4/groups/' + this.$route.params.list + '/status'
+ break
default:
url = '/api/v4/users/' + this.$route.params.user + '/status'
}
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -63,9 +63,13 @@
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
Route::get('domains/{id}/status', 'API\V4\DomainsController@status');
+ Route::apiResource('groups', API\V4\GroupsController::class);
+ Route::get('groups/{id}/status', 'API\V4\GroupsController@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}/skus', 'API\V4\SkusController@userSkus');
Route::get('users/{id}/status', 'API\V4\UsersController@status');
diff --git a/src/tests/Browser/DistlistTest.php b/src/tests/Browser/DistlistTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/DistlistTest.php
@@ -0,0 +1,266 @@
+deleteTestGroup('group-test@kolab.org');
+ $this->clearBetaEntitlements();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolab.org');
+ $this->clearBetaEntitlements();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test distlist info page (unauthenticated)
+ */
+ public function testInfoUnauth(): void
+ {
+ // Test that the page requires authentication
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/distlist/abc')->on(new Home());
+ });
+ }
+
+ /**
+ * Test distlist list page (unauthenticated)
+ */
+ public function testListUnauth(): void
+ {
+ // Test that the page requires authentication
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/distlists')->on(new Home());
+ });
+ }
+
+ /**
+ * Test distlist list page
+ */
+ public function testList(): void
+ {
+ // Log on the user
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Home())
+ ->submitLogon('john@kolab.org', 'simple123', true)
+ ->on(new Dashboard())
+ ->assertMissing('@links .link-distlists');
+ });
+
+ // Test that Distribution lists page is not accessible without the 'distlist' entitlement
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/distlists')
+ ->assertErrorPage(404);
+ });
+
+ // Create a single group, add beta+distlist entitlements
+ $john = $this->getTestUser('john@kolab.org');
+ $this->addDistlistEntitlement($john);
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+
+ // Test distribution lists page
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Dashboard())
+ ->assertSeeIn('@links .link-distlists', 'Distribution lists')
+ ->click('@links .link-distlists')
+ ->on(new DistlistList())
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->waitFor('tbody tr')
+ ->assertElementsCount('tbody tr', 1)
+ ->assertSeeIn('tbody tr:nth-child(1) a', 'group-test@kolab.org')
+ ->assertText('tbody tr:nth-child(1) svg.text-danger title', 'Not Ready')
+ ->assertMissing('tfoot');
+ });
+ });
+ }
+
+ /**
+ * Test distlist creation/editing/deleting
+ *
+ * @depends testList
+ */
+ public function testCreateUpdateDelete(): void
+ {
+ // Test that the page is not available accessible without the 'distlist' entitlement
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/distlist/new')
+ ->assertErrorPage(404);
+ });
+
+ // Add beta+distlist entitlements
+ $john = $this->getTestUser('john@kolab.org');
+ $this->addDistlistEntitlement($john);
+
+ $this->browse(function (Browser $browser) {
+ // Create a group
+ $browser->visit(new DistlistList())
+ ->assertSeeIn('button.create-list', 'Create list')
+ ->click('button.create-list')
+ ->on(new DistlistInfo())
+ ->assertSeeIn('#distlist-info .card-title', 'New distribution list')
+ ->with('@form', function (Browser $browser) {
+ // Assert form content
+ $browser->assertMissing('#status')
+ ->assertSeeIn('div.row:nth-child(1) label', 'Email')
+ ->assertValue('div.row:nth-child(1) input[type=text]', '')
+ ->assertSeeIn('div.row:nth-child(2) label', 'Recipients')
+ ->assertVisible('div.row:nth-child(2) .list-input')
+ ->with(new ListInput('#members'), function (Browser $browser) {
+ $browser->assertListInputValue([])
+ ->assertValue('@input', '');
+ })
+ ->assertSeeIn('button[type=submit]', 'Submit');
+ })
+ // Test error conditions
+ ->type('#email', 'group-test@kolabnow.com')
+ ->click('button[type=submit]')
+ ->waitFor('#email + .invalid-feedback')
+ ->assertSeeIn('#email + .invalid-feedback', 'The specified domain is not available.')
+ ->assertFocused('#email')
+ ->waitFor('#members + .invalid-feedback')
+ ->assertSeeIn('#members + .invalid-feedback', 'At least one recipient is required.')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ // Test successful group creation
+ ->type('#email', 'group-test@kolab.org')
+ ->with(new ListInput('#members'), function (Browser $browser) {
+ $browser->addListEntry('test1@gmail.com')
+ ->addListEntry('test2@gmail.com');
+ })
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list created successfully.')
+ ->on(new DistlistList())
+ ->assertElementsCount('@table tbody tr', 1);
+
+ // Test group update
+ $browser->click('@table tr:nth-child(1) a')
+ ->on(new DistlistInfo())
+ ->assertSeeIn('#distlist-info .card-title', 'Distribution list')
+ ->with('@form', function (Browser $browser) {
+ // Assert form content
+ $browser->assertSeeIn('div.row:nth-child(1) label', 'Status')
+ ->assertSeeIn('div.row:nth-child(1) span.text-danger', 'Not Ready')
+ ->assertSeeIn('div.row:nth-child(2) label', 'Email')
+ ->assertValue('div.row:nth-child(2) input[type=text]:disabled', 'group-test@kolab.org')
+ ->assertSeeIn('div.row:nth-child(3) label', 'Recipients')
+ ->assertVisible('div.row:nth-child(3) .list-input')
+ ->with(new ListInput('#members'), function (Browser $browser) {
+ $browser->assertListInputValue(['test1@gmail.com', 'test2@gmail.com'])
+ ->assertValue('@input', '');
+ })
+ ->assertSeeIn('button[type=submit]', 'Submit');
+ })
+ // Test error handling
+ ->with(new ListInput('#members'), function (Browser $browser) {
+ $browser->addListEntry('invalid address');
+ })
+ ->click('button[type=submit]')
+ ->waitFor('#members + .invalid-feedback')
+ ->assertSeeIn('#members + .invalid-feedback', 'The specified email address is invalid.')
+ ->assertVisible('#members .input-group:nth-child(4) input.is-invalid')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ // Test successful update
+ ->with(new ListInput('#members'), function (Browser $browser) {
+ $browser->removeListEntry(3)->removeListEntry(2);
+ })
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list updated successfully.')
+ ->assertMissing('.invalid-feedback')
+ ->on(new DistlistList())
+ ->assertElementsCount('@table tbody tr', 1);
+
+ $group = Group::where('email', 'group-test@kolab.org')->first();
+ $this->assertSame(['test1@gmail.com'], $group->members);
+
+ // Test group deletion
+ $browser->click('@table tr:nth-child(1) a')
+ ->on(new DistlistInfo())
+ ->assertSeeIn('button.button-delete', 'Delete list')
+ ->click('button.button-delete')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list deleted successfully.')
+ ->on(new DistlistList())
+ ->assertElementsCount('@table tbody tr', 0)
+ ->assertVisible('@table tfoot');
+
+ $this->assertNull(Group::where('email', 'group-test@kolab.org')->first());
+ });
+ }
+
+ /**
+ * Test distribution list status
+ *
+ * @depends testList
+ */
+ public function testStatus(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $this->addDistlistEntitlement($john);
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+ $group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE;
+ $group->save();
+
+ $this->assertFalse($group->isLdapReady());
+
+ $this->browse(function ($browser) use ($group) {
+ // Test auto-refresh
+ $browser->visit('/distlist/' . $group->id)
+ ->on(new DistlistInfo())
+ ->with(new Status(), function ($browser) {
+ $browser->assertSeeIn('@body', 'We are preparing the distribution list')
+ ->assertProgress(83, 'Creating a distribution list...', 'pending')
+ ->assertMissing('@refresh-button')
+ ->assertMissing('@refresh-text')
+ ->assertMissing('#status-link')
+ ->assertMissing('#status-verify');
+ });
+
+ $group->status |= Group::STATUS_LDAP_READY;
+ $group->save();
+
+ // Test Verify button
+ $browser->waitUntilMissing('@status', 10);
+ });
+
+ // TODO: Test all group statuses on the list
+ }
+
+
+ /**
+ * Register the beta + distlist entitlements for the user
+ */
+ private function addDistlistEntitlement($user): void
+ {
+ // Add beta+distlist entitlements
+ $beta_sku = Sku::where('title', 'beta')->first();
+ $distlist_sku = Sku::where('title', 'distlist')->first();
+ $user->assignSku($beta_sku);
+ $user->assignSku($distlist_sku);
+ }
+}
diff --git a/src/tests/Browser/Pages/DistlistInfo.php b/src/tests/Browser/Pages/DistlistInfo.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/DistlistInfo.php
@@ -0,0 +1,45 @@
+waitFor('@form')
+ ->waitUntilMissing('.app-loader');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@form' => '#distlist-info form',
+ '@status' => '#status-box',
+ ];
+ }
+}
diff --git a/src/tests/Browser/Pages/DistlistList.php b/src/tests/Browser/Pages/DistlistList.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/DistlistList.php
@@ -0,0 +1,45 @@
+assertPathIs($this->url())
+ ->waitUntilMissing('@app .app-loader')
+ ->assertSeeIn('#distlist-list .card-title', 'Distribution lists');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@table' => '#distlist-list table',
+ ];
+ }
+}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -605,8 +605,8 @@
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
->with('@skus', function ($browser) {
- $browser->assertElementsCount('tbody tr', 7)
- // Beta/Meet SKU
+ $browser->assertElementsCount('tbody tr', 8)
+ // Meet SKU
->assertSeeIn('tbody tr:nth-child(6) td.name', 'Voice & Video Conferencing (public beta)')
->assertSeeIn('tr:nth-child(6) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(6) td.selection input')
@@ -624,35 +624,46 @@
'tbody tr:nth-child(7) td.buttons button',
'Access to the private beta program subscriptions'
)
-/*
- // Check Meet, Uncheck Beta, expect Meet unchecked
- ->click('#sku-input-meet')
+ // Distlist SKU
+ ->assertSeeIn('tbody tr:nth-child(8) td.name', 'Distribution lists')
+ ->assertSeeIn('tr:nth-child(8) td.price', '0,00 CHF/month')
+ ->assertNotChecked('tbody tr:nth-child(8) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(8) td.selection input')
+ ->assertTip(
+ 'tbody tr:nth-child(8) td.buttons button',
+ 'Access to mail distribution lists'
+ )
+ // Check Distlist, Uncheck Beta, expect Distlist unchecked
+ ->click('#sku-input-distlist')
->click('#sku-input-beta')
->assertNotChecked('#sku-input-beta')
- ->assertNotChecked('#sku-input-meet')
- // Click Meet expect an alert
- ->click('#sku-input-meet')
- ->assertDialogOpened('Video chat requires Beta program.')
+ ->assertNotChecked('#sku-input-distlist')
+ // Click Distlist expect an alert
+ ->click('#sku-input-distlist')
+ ->assertDialogOpened('Distribution lists requires Private Beta (invitation only).')
->acceptDialog()
-*/
- // Enable Meet and submit
- ->click('#sku-input-meet');
+ // Enable Beta and Distlist and submit
+ ->click('#sku-input-beta')
+ ->click('#sku-input-distlist');
})
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
- $expected = ['beta', 'groupware', 'mailbox', 'meet', 'storage', 'storage'];
+ $expected = ['beta', 'distlist', 'groupware', 'mailbox', 'storage', 'storage'];
$this->assertUserEntitlements($john, $expected);
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
->click('#sku-input-beta')
- ->click('#sku-input-meet')
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
$expected = ['groupware', 'mailbox', 'storage', 'storage'];
$this->assertUserEntitlements($john, $expected);
});
+
+ // TODO: Test that the Distlist SKU is not available for users that aren't a group account owners
+ // TODO: Test that entitlements change has immediate effect on the available items in dashboard
+ // i.e. does not require a page reload nor re-login.
}
}
diff --git a/src/tests/Feature/Controller/GroupsTest.php b/src/tests/Feature/Controller/GroupsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/GroupsTest.php
@@ -0,0 +1,492 @@
+deleteTestGroup('group-test@kolab.org');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolab.org');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test group deleting (DELETE /api/v4/groups/)
+ */
+ public function testDestroy(): void
+ {
+ // First create some groups to delete
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+
+ // Test unauth access
+ $response = $this->delete("api/v4/groups/{$group->id}");
+ $response->assertStatus(401);
+
+ // Test non-existing group
+ $response = $this->actingAs($john)->delete("api/v4/groups/abc");
+ $response->assertStatus(404);
+
+ // Test access to other user's group
+ $response = $this->actingAs($jack)->delete("api/v4/groups/{$group->id}");
+ $response->assertStatus(403);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("Access denied", $json['message']);
+ $this->assertCount(2, $json);
+
+ // Test removing a group
+ $response = $this->actingAs($john)->delete("api/v4/groups/{$group->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertEquals('success', $json['status']);
+ $this->assertEquals("Distribution list deleted successfully.", $json['message']);
+ }
+
+ /**
+ * Test groups listing (GET /api/v4/groups)
+ */
+ public function testIndex(): void
+ {
+ $jack = $this->getTestUser('jack@kolab.org');
+ $john = $this->getTestUser('john@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+
+ // Test unauth access
+ $response = $this->get("api/v4/groups");
+ $response->assertStatus(401);
+
+ // Test a user with no groups
+ $response = $this->actingAs($jack)->get("/api/v4/groups");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(0, $json);
+
+ // Test a user with a single group
+ $response = $this->actingAs($john)->get("/api/v4/groups");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ $this->assertSame($group->id, $json[0]['id']);
+ $this->assertSame($group->email, $json[0]['email']);
+ $this->assertArrayHasKey('isDeleted', $json[0]);
+ $this->assertArrayHasKey('isSuspended', $json[0]);
+ $this->assertArrayHasKey('isActive', $json[0]);
+ $this->assertArrayHasKey('isLdapReady', $json[0]);
+
+ // Test that another wallet controller has access to groups
+ $response = $this->actingAs($ned)->get("/api/v4/groups");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ $this->assertSame($group->email, $json[0]['email']);
+ }
+
+ /**
+ * Test fetching group data/profile (GET /api/v4/groups/)
+ */
+ public function testShow(): void
+ {
+ $jack = $this->getTestUser('jack@kolab.org');
+ $john = $this->getTestUser('john@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+
+ // Test unauthorized access to a profile of other user
+ $response = $this->get("/api/v4/groups/{$group->id}");
+ $response->assertStatus(401);
+
+ // Test unauthorized access to a group of another user
+ $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}");
+ $response->assertStatus(403);
+
+ // John: Group owner - non-existing group
+ $response = $this->actingAs($john)->get("/api/v4/groups/abc");
+ $response->assertStatus(404);
+
+ // John: Group owner
+ $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($group->id, $json['id']);
+ $this->assertSame($group->email, $json['email']);
+ $this->assertSame($group->members, $json['members']);
+ $this->assertTrue(!empty($json['statusInfo']));
+ $this->assertArrayHasKey('isDeleted', $json);
+ $this->assertArrayHasKey('isSuspended', $json);
+ $this->assertArrayHasKey('isActive', $json);
+ $this->assertArrayHasKey('isLdapReady', $json);
+ }
+
+ /**
+ * Test fetching group status (GET /api/v4/groups//status)
+ * and forcing setup process update (?refresh=1)
+ */
+ public function testStatus(): void
+ {
+ Queue::fake();
+
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+
+ // Test unauthorized access
+ $response = $this->get("/api/v4/groups/abc/status");
+ $response->assertStatus(401);
+
+ // Test unauthorized access
+ $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}/status");
+ $response->assertStatus(403);
+
+ $group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE;
+ $group->save();
+
+ // Get group status
+ $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertFalse($json['isLdapReady']);
+ $this->assertFalse($json['isReady']);
+ $this->assertFalse($json['isSuspended']);
+ $this->assertTrue($json['isActive']);
+ $this->assertFalse($json['isDeleted']);
+ $this->assertCount(6, $json['process']);
+ $this->assertSame('distlist-new', $json['process'][0]['label']);
+ $this->assertSame(true, $json['process'][0]['state']);
+ $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']);
+ $this->assertSame(false, $json['process'][1]['state']);
+ $this->assertTrue(empty($json['status']));
+ $this->assertTrue(empty($json['message']));
+
+ // Make sure the domain is confirmed (other test might unset that status)
+ $domain = $this->getTestDomain('kolab.org');
+ $domain->status |= \App\Domain::STATUS_CONFIRMED;
+ $domain->save();
+
+ // Now "reboot" the process and the group
+ $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status?refresh=1");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertTrue($json['isLdapReady']);
+ $this->assertTrue($json['isReady']);
+ $this->assertCount(6, $json['process']);
+ $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']);
+ $this->assertSame(true, $json['process'][1]['state']);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('Setup process finished successfully.', $json['message']);
+
+ // Test a case when a domain is not ready
+ $domain->status ^= \App\Domain::STATUS_CONFIRMED;
+ $domain->save();
+
+ $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status?refresh=1");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertTrue($json['isLdapReady']);
+ $this->assertTrue($json['isReady']);
+ $this->assertCount(6, $json['process']);
+ $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']);
+ $this->assertSame(true, $json['process'][1]['state']);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('Setup process finished successfully.', $json['message']);
+ }
+
+ /**
+ * Test GroupsController::statusInfo()
+ */
+ public function testStatusInfo(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+ $group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE;
+ $group->save();
+
+ $result = GroupsController::statusInfo($group);
+
+ $this->assertFalse($result['isReady']);
+ $this->assertCount(6, $result['process']);
+ $this->assertSame('distlist-new', $result['process'][0]['label']);
+ $this->assertSame(true, $result['process'][0]['state']);
+ $this->assertSame('distlist-ldap-ready', $result['process'][1]['label']);
+ $this->assertSame(false, $result['process'][1]['state']);
+ $this->assertSame('running', $result['processState']);
+
+ $group->created_at = Carbon::now()->subSeconds(181);
+ $group->save();
+
+ $result = GroupsController::statusInfo($group);
+
+ $this->assertSame('failed', $result['processState']);
+
+ $group->status |= Group::STATUS_LDAP_READY;
+ $group->save();
+
+ $result = GroupsController::statusInfo($group);
+
+ $this->assertTrue($result['isReady']);
+ $this->assertCount(6, $result['process']);
+ $this->assertSame('distlist-new', $result['process'][0]['label']);
+ $this->assertSame(true, $result['process'][0]['state']);
+ $this->assertSame('distlist-ldap-ready', $result['process'][1]['label']);
+ $this->assertSame(true, $result['process'][2]['state']);
+ $this->assertSame('done', $result['processState']);
+ }
+
+ /**
+ * Test group creation (POST /api/v4/groups)
+ */
+ public function testStore(): void
+ {
+ Queue::fake();
+
+ $jack = $this->getTestUser('jack@kolab.org');
+ $john = $this->getTestUser('john@kolab.org');
+
+ // Test unauth request
+ $response = $this->post("/api/v4/groups", []);
+ $response->assertStatus(401);
+
+ // Test non-controller user
+ $response = $this->actingAs($jack)->post("/api/v4/groups", []);
+ $response->assertStatus(403);
+
+ // Test empty request
+ $response = $this->actingAs($john)->post("/api/v4/groups", []);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("The email field is required.", $json['errors']['email']);
+ $this->assertCount(2, $json);
+
+ // Test missing members
+ $post = ['email' => 'group-test@kolab.org'];
+ $response = $this->actingAs($john)->post("/api/v4/groups", $post);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("At least one recipient is required.", $json['errors']['members']);
+ $this->assertCount(2, $json);
+
+ // Test invalid email
+ $post = ['email' => 'invalid'];
+ $response = $this->actingAs($john)->post("/api/v4/groups", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json);
+ $this->assertSame('The specified email is invalid.', $json['errors']['email']);
+
+ // Test successful group creation
+ $post = [
+ 'email' => 'group-test@kolab.org',
+ 'members' => ['test1@domain.tld', 'test2@domain.tld']
+ ];
+
+ $response = $this->actingAs($john)->post("/api/v4/groups", $post);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Distribution list created successfully.", $json['message']);
+ $this->assertCount(2, $json);
+
+ $group = Group::where('email', 'group-test@kolab.org')->first();
+ $this->assertInstanceOf(Group::class, $group);
+ $this->assertSame($post['email'], $group->email);
+ $this->assertSame($post['members'], $group->members);
+ $this->assertTrue($john->groups()->get()->contains($group));
+ }
+
+ /**
+ * Test group update (PUT /api/v4/groups/)
+ */
+ public function testUpdate(): void
+ {
+ Queue::fake();
+
+ $jack = $this->getTestUser('jack@kolab.org');
+ $john = $this->getTestUser('john@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->assignToWallet($john->wallets->first());
+
+ // Test unauthorized update
+ $response = $this->get("/api/v4/groups/{$group->id}", []);
+ $response->assertStatus(401);
+
+ // Test unauthorized update
+ $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}", []);
+ $response->assertStatus(403);
+
+ // Test updating - missing members
+ $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", []);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("At least one recipient is required.", $json['errors']['members']);
+ $this->assertCount(2, $json);
+
+ // Test some invalid data
+ $post = ['members' => ['test@domain.tld', 'invalid']];
+ $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json);
+ $this->assertSame('The specified email address is invalid.', $json['errors']['members'][1]);
+
+ // Valid data - members changed
+ $post = [
+ 'members' => ['member1@test.domain', 'member2@test.domain']
+ ];
+
+ $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", $post);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Distribution list updated successfully.", $json['message']);
+ $this->assertCount(2, $json);
+ $this->assertSame($group->fresh()->members, $post['members']);
+ }
+
+ /**
+ * Group email address validation.
+ */
+ public function testValidateGroupEmail(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $group = $this->getTestGroup('group-test@kolab.org');
+
+ // Invalid email
+ $result = GroupsController::validateGroupEmail('', $john);
+ $this->assertSame("The email field is required.", $result);
+
+ $result = GroupsController::validateGroupEmail('kolab.org', $john);
+ $this->assertSame("The specified email is invalid.", $result);
+
+ $result = GroupsController::validateGroupEmail('.@kolab.org', $john);
+ $this->assertSame("The specified email is invalid.", $result);
+
+ $result = GroupsController::validateGroupEmail('test123456@localhost', $john);
+ $this->assertSame("The specified domain is invalid.", $result);
+
+ $result = GroupsController::validateGroupEmail('test123456@unknown-domain.org', $john);
+ $this->assertSame("The specified domain is invalid.", $result);
+
+ // forbidden public domain
+ $result = GroupsController::validateGroupEmail('testtest@kolabnow.com', $john);
+ $this->assertSame("The specified domain is not available.", $result);
+
+ // existing alias
+ $result = GroupsController::validateGroupEmail('jack.daniels@kolab.org', $john);
+ $this->assertSame("The specified email is not available.", $result);
+
+ // existing user
+ $result = GroupsController::validateGroupEmail('ned@kolab.org', $john);
+ $this->assertSame("The specified email is not available.", $result);
+
+ // existing group
+ $result = GroupsController::validateGroupEmail('group-test@kolab.org', $john);
+ $this->assertSame("The specified email is not available.", $result);
+
+ // valid
+ $result = GroupsController::validateGroupEmail('admin@kolab.org', $john);
+ $this->assertSame(null, $result);
+ }
+
+ /**
+ * Group member email address validation.
+ */
+ public function testValidateMemberEmail(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+
+ // Invalid format
+ $result = GroupsController::validateMemberEmail('kolab.org', $john);
+ $this->assertSame("The specified email address is invalid.", $result);
+
+ $result = GroupsController::validateMemberEmail('.@kolab.org', $john);
+ $this->assertSame("The specified email address is invalid.", $result);
+
+ $result = GroupsController::validateMemberEmail('test123456@localhost', $john);
+ $this->assertSame("The specified email address is invalid.", $result);
+
+ // Test local non-existing user
+ $result = GroupsController::validateMemberEmail('unknown@kolab.org', $john);
+ $this->assertSame("The specified email address does not exist.", $result);
+
+ // Test local existing user
+ $result = GroupsController::validateMemberEmail('ned@kolab.org', $john);
+ $this->assertSame(null, $result);
+
+ // Test existing user, but not in the same account
+ $result = GroupsController::validateMemberEmail('jeroen@jeroen.jeroen', $john);
+ $this->assertSame(null, $result);
+
+ // Valid address
+ $result = GroupsController::validateMemberEmail('test@google.com', $john);
+ $this->assertSame(null, $result);
+ }
+}
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
@@ -1177,7 +1177,6 @@
$domain = reset($public_domains);
$john = $this->getTestUser('john@kolab.org');
- $jack = $this->getTestUser('jack@kolab.org');
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
return [
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -35,9 +35,12 @@
*/
protected function clearBetaEntitlements(): void
{
- $betas = \App\Sku::where('handler_class', 'like', 'App\\Handlers\\Beta\\%')
- ->orWhere('handler_class', 'App\Handlers\Beta')
- ->pluck('id')->all();
+ $beta_handlers = [
+ 'App\Handlers\Beta',
+ 'App\Handlers\Distlist',
+ ];
+
+ $betas = \App\Sku::whereIn('handler_class', $beta_handlers)->pluck('id')->all();
\App\Entitlement::whereIn('sku_id', $betas)->delete();
}