diff --git a/docker/tests/init.sh b/docker/tests/init.sh index dbc98f3f..d4aa5f22 100755 --- a/docker/tests/init.sh +++ b/docker/tests/init.sh @@ -1,75 +1,75 @@ #!/bin/bash #set -e sudo cp -a /src/kolabsrc.orig /src/kolabsrc sudo chmod 777 -R /src/kolabsrc cd /src/kolabsrc sudo rm -rf vendor/ composer.lock php -dmemory_limit=-1 $(command -v composer) install sudo rm -rf node_modules mkdir node_modules npm install find bootstrap/cache/ -type f ! -name ".gitignore" -delete ./artisan key:generate ./artisan clear-compiled ./artisan cache:clear ./artisan horizon:install if [ ! -f storage/oauth-public.key -o ! -f storage/oauth-private.key ]; then ./artisan passport:keys --force fi cat >> .env << EOF PASSPORT_PRIVATE_KEY="$(cat storage/oauth-private.key)" PASSPORT_PUBLIC_KEY="$(cat storage/oauth-public.key)" EOF if rpm -qv chromium 2>/dev/null; then chver=$(rpmquery --queryformat="%{VERSION}" chromium | awk -F'.' '{print $1}') ./artisan dusk:chrome-driver ${chver} fi if [ ! -f 'resources/countries.php' ]; then ./artisan data:countries fi npm run dev # /usr/bin/chromium-browser --no-sandbox --headless --disable-gpu --remote-debugging-port=9222 http://localhost & rm -rf database/database.sqlite ./artisan db:ping --wait php -dmemory_limit=512M ./artisan migrate:refresh --seed ./artisan data:import || : # nohup ./artisan horizon >/dev/null 2>&1 & ./artisan octane:start --host=$(grep OCTANE_HTTP_HOST .env | tail -n1 | sed "s/OCTANE_HTTP_HOST=//") >/dev/null 2>&1 & php \ -dmemory_limit=-1 \ vendor/bin/phpunit \ - --exclude flaky \ + --exclude-group skipci \ --verbose \ --stop-on-defect \ --stop-on-error \ --stop-on-failure \ --testsuite Unit php \ -dmemory_limit=-1 \ vendor/bin/phpunit \ - --exclude flaky \ + --exclude-group skipci \ --verbose \ --stop-on-defect \ --stop-on-error \ --stop-on-failure \ --testsuite Functional php \ -dmemory_limit=-1 \ vendor/bin/phpunit \ - --exclude flaky \ + --exclude-group skipci,coinbase,mollie,stripe,meet,dns \ --verbose \ --stop-on-defect \ --stop-on-error \ --stop-on-failure \ --testsuite Feature diff --git a/src/tests/Browser/Admin/DashboardTest.php b/src/tests/Browser/Admin/DashboardTest.php index 9510d351..23c8c3ed 100644 --- a/src/tests/Browser/Admin/DashboardTest.php +++ b/src/tests/Browser/Admin/DashboardTest.php @@ -1,155 +1,157 @@ getTestUser('jack@kolab.org'); $jack->setSetting('external_email', null); $this->deleteTestUser('test@testsearch.com'); $this->deleteTestDomain('testsearch.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $jack = $this->getTestUser('jack@kolab.org'); $jack->setSetting('external_email', null); $this->deleteTestUser('test@testsearch.com'); $this->deleteTestDomain('testsearch.com'); parent::tearDown(); } /** * Test user search + * @group skipci */ public function testSearch(): void { $this->browse(function (Browser $browser) { $browser->visit(new Home()) ->submitLogon('jeroen@jeroen.jeroen', \App\Utils::generatePassphrase(), true) ->on(new Dashboard()) ->assertFocused('@search input') ->assertMissing('@search table'); // Test search with no results $browser->type('@search input', 'unknown') ->click('@search form button') ->assertToast(Toast::TYPE_INFO, '0 user accounts have been found.') ->assertMissing('@search table'); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $jack->setSetting('external_email', 'john.doe.external@gmail.com'); // Test search with multiple results $browser->type('@search input', 'john.doe.external@gmail.com') ->click('@search form button') ->assertToast(Toast::TYPE_INFO, '2 user accounts have been found.') ->whenAvailable('@search table', function (Browser $browser) use ($john, $jack) { $browser->assertElementsCount('tbody tr', 2) ->with('tbody tr:first-child', function (Browser $browser) use ($jack) { $browser->assertSeeIn('td:nth-child(1) a', $jack->email) ->assertSeeIn('td:nth-child(2) a', $jack->id); if ($browser->isPhone()) { $browser->assertMissing('td:nth-child(3)'); } else { $browser->assertVisible('td:nth-child(3)') ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') ->assertVisible('td:nth-child(4)') ->assertText('td:nth-child(4)', ''); } }) ->with('tbody tr:last-child', function (Browser $browser) use ($john) { $browser->assertSeeIn('td:nth-child(1) a', $john->email) ->assertSeeIn('td:nth-child(2) a', $john->id); if ($browser->isPhone()) { $browser->assertMissing('td:nth-child(3)'); } else { $browser->assertVisible('td:nth-child(3)') ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') ->assertVisible('td:nth-child(4)') ->assertText('td:nth-child(4)', ''); } }); }); // Test search with single record result -> redirect to user page $browser->type('@search input', 'kolab.org') ->click('@search form button') ->assertMissing('@search table') ->waitForLocation('/user/' . $john->id) ->waitUntilMissing('.app-loader') ->whenAvailable('#user-info', function (Browser $browser) use ($john) { $browser->assertSeeIn('.card-title', $john->email); }); }); } /** * Test user search deleted user/domain + * @group skipci */ public function testSearchDeleted(): void { $this->browse(function (Browser $browser) { $browser->visit(new Home()) ->submitLogon('jeroen@jeroen.jeroen', \App\Utils::generatePassphrase(), true) ->on(new Dashboard()) ->assertFocused('@search input') ->assertMissing('@search table'); // Deleted users/domains $domain = $this->getTestDomain('testsearch.com', ['type' => \App\Domain::TYPE_EXTERNAL]); $user = $this->getTestUser('test@testsearch.com'); $plan = \App\Plan::where('title', 'group')->first(); $user->assignPlan($plan, $domain); $user->setAliases(['alias@testsearch.com']); Queue::fake(); $user->delete(); // Test search with multiple results $browser->type('@search input', 'testsearch.com') ->click('@search form button') ->assertToast(Toast::TYPE_INFO, '1 user accounts have been found.') ->whenAvailable('@search table', function (Browser $browser) use ($user) { $browser->assertElementsCount('tbody tr', 1) ->assertVisible('tbody tr:first-child.text-secondary') ->with('tbody tr:first-child', function (Browser $browser) use ($user) { $browser->assertSeeIn('td:nth-child(1) span', $user->email) ->assertSeeIn('td:nth-child(2) span', $user->id); if ($browser->isPhone()) { $browser->assertMissing('td:nth-child(3)'); } else { $browser->assertVisible('td:nth-child(3)') ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') ->assertVisible('td:nth-child(4)') ->assertTextRegExp('td:nth-child(4)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/'); } }); }); }); } } diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php index c1b08920..f6c032b1 100644 --- a/src/tests/Browser/DomainTest.php +++ b/src/tests/Browser/DomainTest.php @@ -1,351 +1,357 @@ deleteTestDomain('testdomain.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestDomain('testdomain.com'); parent::tearDown(); } /** * Test domain info page (unauthenticated) */ public function testDomainInfoUnauth(): void { // Test that the page requires authentication $this->browse(function ($browser) { $browser->visit('/domain/123')->on(new Home()); }); } /** * Test domains list page (unauthenticated) */ public function testDomainListUnauth(): void { // Test that the page requires authentication $this->browse(function ($browser) { $browser->visit('/domains')->on(new Home()); }); } /** * Test domain info page (non-existing domain id) + * @group skipci */ public function testDomainInfo404(): void { $this->browse(function ($browser) { // FIXME: I couldn't make loginAs() method working // Note: Here we're also testing that unauthenticated request // is passed to logon form and then "redirected" to the requested page $browser->visit('/domain/123') ->on(new Home()) ->submitLogon('john@kolab.org', 'simple123') ->assertErrorPage(404); }); } /** * Test domain info page (existing domain) * * @depends testDomainInfo404 + * @group skipci */ public function testDomainInfo(): void { $this->browse(function ($browser) { // Unconfirmed domain $domain = Domain::where('namespace', 'kolab.org')->first(); if ($domain->isConfirmed()) { $domain->status ^= Domain::STATUS_CONFIRMED; $domain->save(); } $domain->setSetting('spf_whitelist', \json_encode(['.test.com'])); $browser->visit('/domain/' . $domain->id) ->on(new DomainInfo()) ->assertSeeIn('.card-title', 'Domain') ->whenAvailable('@general', function ($browser) use ($domain) { $browser->assertSeeIn('form div:nth-child(1) label', 'Status') ->assertSeeIn('form div:nth-child(1) #status.text-danger', 'Not Ready') ->assertSeeIn('form div:nth-child(2) label', 'Name') ->assertValue('form div:nth-child(2) input:disabled', $domain->namespace) ->assertSeeIn('form div:nth-child(3) label', 'Subscriptions'); }) ->whenAvailable('@general form div:nth-child(3) table', function ($browser) { $browser->assertElementsCount('tbody tr', 1) ->assertVisible('tbody tr td.selection input:checked:disabled') ->assertSeeIn('tbody tr td.name', 'External Domain') ->assertSeeIn('tbody tr td.price', '0,00 CHF/month') ->assertTip( 'tbody tr td.buttons button', 'Host a domain that is externally registered' ); }) ->whenAvailable('@verify', function ($browser) use ($domain) { $browser->assertSeeIn('pre', $domain->namespace) ->assertSeeIn('pre', $domain->hash()) ->click('button') ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.'); // TODO: Test scenario when a domain confirmation failed }) ->whenAvailable('@config', function ($browser) use ($domain) { $browser->assertSeeIn('pre', $domain->namespace); }) ->assertMissing('@general button[type=submit]') ->assertMissing('@verify'); // Check that confirmed domain page contains only the config box $browser->visit('/domain/' . $domain->id) ->on(new DomainInfo()) ->assertMissing('@verify') ->assertPresent('@config'); }); } /** * Test domain settings + * @group skipci */ public function testDomainSettings(): void { $this->browse(function ($browser) { $domain = Domain::where('namespace', 'kolab.org')->first(); $domain->setSetting('spf_whitelist', \json_encode(['.test.com'])); $browser->visit('/domain/' . $domain->id) ->on(new DomainInfo()) ->assertElementsCount('@nav a', 2) ->assertSeeIn('@nav #tab-general', 'General') ->assertSeeIn('@nav #tab-settings', 'Settings') ->click('@nav #tab-settings') ->with('#settings form', function (Browser $browser) { // Test whitelist widget $widget = new ListInput('#spf_whitelist'); $browser->assertSeeIn('div.row:nth-child(1) label', 'SPF Whitelist') ->assertVisible('div.row:nth-child(1) .list-input') ->with($widget, function (Browser $browser) { $browser->assertListInputValue(['.test.com']) ->assertValue('@input', '') ->addListEntry('invalid domain'); }) ->click('button[type=submit]') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->with($widget, function (Browser $browser) { $err = 'The entry format is invalid. Expected a domain name starting with a dot.'; $browser->assertFormError(2, $err, false) ->removeListEntry(2) ->removeListEntry(1) ->addListEntry('.new.domain.tld'); }) ->click('button[type=submit]') ->assertToast(Toast::TYPE_SUCCESS, 'Domain settings updated successfully.'); }); }); } /** * Test domains list page * * @depends testDomainListUnauth + * @group skipci */ public function testDomainList(): void { $this->browse(function ($browser) { // Login the user $browser->visit('/login') ->on(new Home()) ->submitLogon('john@kolab.org', 'simple123', true) // On dashboard click the "Domains" link ->on(new Dashboard()) ->assertSeeIn('@links a.link-domains', 'Domains') ->click('@links a.link-domains') // On Domains List page click the domain entry ->on(new DomainList()) ->waitFor('@table tbody tr') ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-globe.text-success') ->assertText('@table tbody tr:first-child td:first-child svg title', 'Active') ->assertSeeIn('@table tbody tr:first-child td:first-child', 'kolab.org') ->assertMissing('@table tfoot') ->click('@table tbody tr:first-child td:first-child a') // On Domain Info page verify that's the clicked domain ->on(new DomainInfo()) ->whenAvailable('@config', function ($browser) { $browser->assertSeeIn('pre', 'kolab.org'); }); }); // TODO: Test domains list acting as Ned (John's "delegatee") } /** * Test domains list page (user with no domains) */ public function testDomainListEmpty(): void { $this->browse(function ($browser) { // Login the user $browser->visit('/login') ->on(new Home()) ->submitLogon('jack@kolab.org', 'simple123', true) ->on(new Dashboard()) ->assertVisible('@links a.link-profile') ->assertMissing('@links a.link-domains') ->assertMissing('@links a.link-users') ->assertMissing('@links a.link-wallet'); /* // On dashboard click the "Domains" link ->assertSeeIn('@links a.link-domains', 'Domains') ->click('@links a.link-domains') // On Domains List page click the domain entry ->on(new DomainList()) ->assertMissing('@table tbody') ->assertSeeIn('tfoot td', 'There are no domains in this account.'); */ }); } /** * Test domain creation page + * @group skipci */ public function testDomainCreate(): void { $this->browse(function ($browser) { $browser->visit('/login') ->on(new Home()) ->submitLogon('john@kolab.org', 'simple123', true) ->visit('/domains') ->on(new DomainList()) ->assertSeeIn('.card-title button.btn-success', 'Create domain') ->click('.card-title button.btn-success') ->on(new DomainInfo()) ->assertSeeIn('.card-title', 'New domain') ->assertElementsCount('@nav li', 1) ->assertSeeIn('@nav li:first-child', 'General') ->whenAvailable('@general', function ($browser) { $browser->assertSeeIn('form div:nth-child(1) label', 'Name') ->assertValue('form div:nth-child(1) input:not(:disabled)', '') ->assertFocused('form div:nth-child(1) input') ->assertSeeIn('form div:nth-child(2) label', 'Package') ->assertMissing('form div:nth-child(3)'); }) ->whenAvailable('@general form div:nth-child(2) table', function ($browser) { $browser->assertElementsCount('tbody tr', 1) ->assertVisible('tbody tr td.selection input:checked[readonly]') ->assertSeeIn('tbody tr td.name', 'Domain Hosting') ->assertSeeIn('tbody tr td.price', '0,00 CHF/month') ->assertTip( 'tbody tr td.buttons button', 'Use your own, existing domain.' ); }) ->assertSeeIn('@general button.btn-primary[type=submit]', 'Submit') ->assertMissing('@config') ->assertMissing('@verify') ->assertMissing('@settings') ->assertMissing('@status') // Test error handling ->click('button[type=submit]') ->waitFor('#namespace + .invalid-feedback') ->assertSeeIn('#namespace + .invalid-feedback', 'The namespace field is required.') ->assertFocused('#namespace') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->type('@general form div:nth-child(1) input', 'testdomain..com') ->click('button[type=submit]') ->waitFor('#namespace + .invalid-feedback') ->assertSeeIn('#namespace + .invalid-feedback', 'The specified domain is invalid.') ->assertFocused('#namespace') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') // Test success ->type('@general form div:nth-child(1) input', 'testdomain.com') ->click('button[type=submit]') ->assertToast(Toast::TYPE_SUCCESS, 'Domain created successfully.') ->on(new DomainList()) ->assertSeeIn('@table tr:nth-child(2) a', 'testdomain.com'); }); } /** * Test domain deletion + * @group skipci */ 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/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php index ebe51586..1b419ae1 100644 --- a/src/tests/Feature/Controller/DomainsTest.php +++ b/src/tests/Feature/Controller/DomainsTest.php @@ -1,607 +1,608 @@ deleteTestUser('test1@' . \config('app.domain')); $this->deleteTestUser('test2@' . \config('app.domain')); $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); Sku::where('title', 'test')->delete(); } 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'); Sku::where('title', 'test')->delete(); $domain = $this->getTestDomain('kolab.org'); $domain->settings()->whereIn('key', ['spf_whitelist'])->delete(); parent::tearDown(); } /** * Test domain confirm request + * @group skipci */ 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'); $user = $this->getTestUser('test1@domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); $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(); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain verified successfully.', $json['message']); $this->assertTrue(is_array($json['statusInfo'])); // Not authorized access $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(403); // Authorized access by additional account controller $domain = $this->getTestDomain('kolab.org'); $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); } /** * Test domain delete request (DELETE /api/v4/domains/) */ 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 { // User with no domains $user = $this->getTestUser('test1@domainscontroller.com'); $response = $this->actingAs($user)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("0 domains have been found.", $json['message']); $this->assertSame([], $json['list']); // User with custom domain(s) $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $response = $this->actingAs($john)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(1, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("1 domains have been found.", $json['message']); $this->assertCount(1, $json['list']); $this->assertSame('kolab.org', $json['list'][0]['namespace']); // Values below are tested by Unit tests $this->assertArrayHasKey('isConfirmed', $json['list'][0]); $this->assertArrayHasKey('isDeleted', $json['list'][0]); $this->assertArrayHasKey('isVerified', $json['list'][0]); $this->assertArrayHasKey('isSuspended', $json['list'][0]); $this->assertArrayHasKey('isActive', $json['list'][0]); $this->assertArrayHasKey('isLdapReady', $json['list'][0]); $response = $this->actingAs($ned)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertCount(1, $json['list']); $this->assertSame('kolab.org', $json['list'][0]['namespace']); } /** * Test domain config update (POST /api/v4/domains//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $domain = $this->getTestDomain('kolab.org'); $domain->setSetting('spf_whitelist', null); // Test unknown domain id $post = ['spf_whitelist' => []]; $response = $this->actingAs($john)->post("/api/v4/domains/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['spf_whitelist' => []]; $response = $this->actingAs($jack)->post("/api/v4/domains/{$domain->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['grey' => 1]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['grey']); $this->assertNull($domain->fresh()->getSetting('spf_whitelist')); // Test some valid data $post = ['spf_whitelist' => ['.test.domain.com']]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame('Domain settings updated successfully.', $json['message']); $expected = \json_encode($post['spf_whitelist']); $this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist')); // Test input validation $post = ['spf_whitelist' => ['aaa']]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame( 'The entry format is invalid. Expected a domain name starting with a dot.', $json['errors']['spf_whitelist'][0] ); $this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist')); } /** * Test fetching domain info */ public function testShow(): void { $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $user = $this->getTestUser('test1@domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); $discount = \App\Discount::withEnvTenantContext()->where('code', 'TEST')->first(); $wallet = $user->wallet(); $wallet->discount()->associate($discount); $wallet->save(); Entitlement::create([ 'wallet_id' => $user->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals($domain->id, $json['id']); $this->assertEquals($domain->namespace, $json['namespace']); $this->assertEquals($domain->status, $json['status']); $this->assertEquals($domain->type, $json['type']); $this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']); $this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']); $this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']); $this->assertSame([], $json['config']['spf_whitelist']); $this->assertCount(4, $json['mx']); $this->assertTrue(strpos(implode("\n", $json['mx']), $domain->namespace) !== false); $this->assertCount(8, $json['dns']); $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false); $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false); $this->assertTrue(is_array($json['statusInfo'])); // Values below are tested by Unit tests $this->assertArrayHasKey('isConfirmed', $json); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isVerified', $json); $this->assertArrayHasKey('isSuspended', $json); $this->assertArrayHasKey('isActive', $json); $this->assertArrayHasKey('isLdapReady', $json); $this->assertCount(1, $json['skus']); $this->assertSame(1, $json['skus'][$sku_domain->id]['count']); $this->assertSame([0], $json['skus'][$sku_domain->id]['costs']); $this->assertSame($wallet->id, $json['wallet']['id']); $this->assertSame($wallet->balance, $json['wallet']['balance']); $this->assertSame($wallet->currency, $json['wallet']['currency']); $this->assertSame($discount->discount, $json['wallet']['discount']); $this->assertSame($discount->description, $json['wallet']['discount_description']); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); // Not authorized - Other account domain $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(403); $domain = $this->getTestDomain('kolab.org'); // Ned is an additional controller on kolab.org's wallet $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(200); // Jack has no entitlement/control over kolab.org $response = $this->actingAs($jack)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(403); } /** * Test fetching SKUs list for a domain (GET /domains//skus) */ public function testSkus(): void { $user = $this->getTestUser('john@kolab.org'); $domain = $this->getTestDomain('kolab.org'); // Unauth access not allowed $response = $this->get("api/v4/domains/{$domain->id}/skus"); $response->assertStatus(401); // Create an sku for another tenant, to make sure it is not included in the result $nsku = Sku::create([ 'title' => 'test', 'name' => 'Test', 'description' => '', 'active' => true, 'cost' => 100, 'handler_class' => 'App\Handlers\Domain', ]); $tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first(); $nsku->tenant_id = $tenant->id; $nsku->save(); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSkuElement('domain-hosting', $json[0], [ 'prio' => 0, 'type' => 'domain', 'handler' => 'DomainHosting', 'enabled' => true, 'readonly' => true, ]); } /** * Test fetching domain status (GET /api/v4/domains//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_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; $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->assertTrue($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(true, $json['process'][3]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); // TODO: Test completing all process steps } /** * Test domain creation (POST /api/v4/domains) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test empty request $response = $this->actingAs($john)->post("/api/v4/domains", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The namespace field is required.", $json['errors']['namespace'][0]); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['namespace']); $this->assertCount(2, $json); // Test access by user not being a wallet controller $post = ['namespace' => 'domainscontroller.com']; $response = $this->actingAs($jack)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['namespace' => '--']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified domain is invalid.', $json['errors']['namespace'][0]); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['namespace']); // Test an existing domain $post = ['namespace' => 'kolab.org']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified domain is not available.', $json['errors']['namespace']); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); // Missing package $post = ['namespace' => 'domainscontroller.com']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Package is required.", $json['errors']['package']); $this->assertCount(2, $json); // Invalid package $post['package'] = $package_kolab->id; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Invalid package selected.", $json['errors']['package']); $this->assertCount(2, $json); // Test full and valid data $post['package'] = $package_domain->id; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Domain created successfully.", $json['message']); $this->assertCount(2, $json); $domain = Domain::where('namespace', $post['namespace'])->first(); $this->assertInstanceOf(Domain::class, $domain); // Assert the new domain entitlements $this->assertEntitlements($domain, ['domain-hosting']); // Assert the wallet to which the new domain should be assigned to $wallet = $domain->wallet(); $this->assertSame($john->wallets->first()->id, $wallet->id); // Test 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->entitlements()->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(); } }