diff --git a/src/app/Policy/Greylist/Request.php b/src/app/Policy/Greylist/Request.php index 493ca04d..0b364131 100644 --- a/src/app/Policy/Greylist/Request.php +++ b/src/app/Policy/Greylist/Request.php @@ -1,299 +1,299 @@ request = $request; if (array_key_exists('timestamp', $this->request)) { $this->timestamp = \Carbon\Carbon::parse($this->request['timestamp']); } else { $this->timestamp = \Carbon\Carbon::now(); } } public function headerGreylist() { if ($this->whitelist) { if ($this->whitelist->sender_local) { return sprintf( "Received-Greylist: sender %s whitelisted since %s", $this->sender, $this->whitelist->created_at->toDateString() ); } return sprintf( "Received-Greylist: domain %s from %s whitelisted since %s (UTC)", $this->senderDomain, $this->request['client_address'], $this->whitelist->created_at->toDateTimeString() ); } $connect = $this->findConnectsCollection()->orderBy('created_at')->first(); if ($connect) { return sprintf( "Received-Greylist: greylisted from %s until %s.", $connect->created_at, $this->timestamp ); } return "Received-Greylist: no opinion here"; } public function shouldDefer() { $deferIfPermit = true; list($this->netID, $this->netType) = \App\Utils::getNetFromAddress($this->request['client_address']); if (!$this->netID) { return true; } $recipient = $this->recipientFromRequest(); $this->sender = $this->senderFromRequest(); list($this->senderLocal, $this->senderDomain) = explode('@', $this->sender); $entry = $this->findConnectsCollectionRecent()->orderBy('updated_at')->first(); if (!$entry) { // purge all entries to avoid a unique constraint violation. $this->findConnectsCollection()->delete(); - $entry = \App\Greylist\Connect::create( + $entry = Connect::create( [ 'sender_local' => $this->senderLocal, 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'recipient_hash' => $this->recipientHash, 'recipient_id' => $this->recipientID, 'recipient_type' => $this->recipientType, 'connect_count' => 1, 'created_at' => $this->timestamp, 'updated_at' => $this->timestamp ] ); } // see if all recipients and their domains are opt-outs $enabled = false; if ($recipient) { - $setting = \App\Greylist\Setting::where( + $setting = Setting::where( [ 'object_id' => $this->recipientID, 'object_type' => $this->recipientType, 'key' => 'greylist_enabled' ] )->first(); if (!$setting) { - $setting = \App\Greylist\Setting::where( + $setting = Setting::where( [ 'object_id' => $recipient->domain()->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled' ] )->first(); if (!$setting) { $enabled = true; } else { if ($setting->{'value'} !== 'false') { $enabled = true; } } } else { if ($setting->{'value'} !== 'false') { $enabled = true; } } } else { $enabled = true; } // the following block is to maintain statistics and state ... - $entries = \App\Greylist\Connect::where( + $entries = Connect::where( [ 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType ] ) ->whereDate('updated_at', '>=', $this->timestamp->copy()->subDays(7)); // determine if the sender domain is a whitelist from this network - $this->whitelist = \App\Greylist\Whitelist::where( + $this->whitelist = Whitelist::where( [ 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType ] )->first(); if ($this->whitelist) { if ($this->whitelist->updated_at < $this->timestamp->copy()->subMonthsWithoutOverflow(1)) { $this->whitelist->delete(); } else { $this->whitelist->updated_at = $this->timestamp; $this->whitelist->save(['timestamps' => false]); $entries->update( [ 'greylisting' => false, 'updated_at' => $this->timestamp ] ); return false; } } else { if ($entries->count() >= 5) { - $this->whitelist = \App\Greylist\Whitelist::create( + $this->whitelist = Whitelist::create( [ 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'created_at' => $this->timestamp, 'updated_at' => $this->timestamp ] ); $entries->update( [ 'greylisting' => false, 'updated_at' => $this->timestamp ] ); } } // TODO: determine if the sender (individual) is a whitelist // TODO: determine if the sender is a penpal of any of the recipients. First recipient wins. if (!$enabled) { return false; } // determine if the sender, net and recipient combination has existed before, for each recipient // any one recipient matching should supersede the other recipients not having matched - $connect = \App\Greylist\Connect::where( + $connect = Connect::where( [ 'sender_local' => $this->senderLocal, 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'recipient_id' => $this->recipientID, 'recipient_type' => $this->recipientType, ] ) ->whereDate('updated_at', '>=', $this->timestamp->copy()->subMonthsWithoutOverflow(1)) ->orderBy('updated_at') ->first(); if (!$connect) { - $connect = \App\Greylist\Connect::create( + $connect = Connect::create( [ 'sender_local' => $this->senderLocal, 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'recipient_id' => $this->recipientID, 'recipient_type' => $this->recipientType, 'connect_count' => 0, 'created_at' => $this->timestamp, 'updated_at' => $this->timestamp ] ); } $connect->connect_count += 1; // TODO: The period of time for which the greylisting persists is configurable. if ($connect->created_at < $this->timestamp->copy()->subMinutes(5)) { $deferIfPermit = false; $connect->greylisting = false; } $connect->save(); return $deferIfPermit; } private function findConnectsCollection() { - $collection = \App\Greylist\Connect::where( + $collection = Connect::where( [ 'sender_local' => $this->senderLocal, 'sender_domain' => $this->senderDomain, 'net_id' => $this->netID, 'net_type' => $this->netType, 'recipient_id' => $this->recipientID, 'recipient_type' => $this->recipientType ] ); return $collection; } private function findConnectsCollectionRecent() { return $this->findConnectsCollection() ->where('updated_at', '>=', $this->timestamp->copy()->subDays(7)); } private function recipientFromRequest() { $recipients = \App\Utils::findObjectsByRecipientAddress($this->request['recipient']); if (sizeof($recipients) > 1) { \Log::warning( "Only taking the first recipient from the request in to account for {$this->request['recipient']}" ); } if (count($recipients) >= 1) { $recipient = $recipients[0]; $this->recipientID = $recipient->id; $this->recipientType = get_class($recipient); } else { $recipient = null; } $this->recipientHash = hash('sha256', $this->request['recipient']); return $recipient; } public function senderFromRequest() { return \App\Utils::normalizeAddress($this->request['sender']); } } diff --git a/src/app/Policy/SPF/Policy.php b/src/app/Policy/SPF/Policy.php deleted file mode 100644 index e69de29b..00000000 diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue index 8b835a65..4593ca5a 100644 --- a/src/resources/vue/Domain/Info.vue +++ b/src/resources/vue/Domain/Info.vue @@ -1,142 +1,142 @@ diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php index 169b0db3..817a873b 100644 --- a/src/tests/Browser/Admin/UserTest.php +++ b/src/tests/Browser/Admin/UserTest.php @@ -1,516 +1,518 @@ getTestUser('john@kolab.org'); $john->setSettings([ 'phone' => '+48123123123', 'external_email' => 'john.doe.external@gmail.com', ]); if ($john->isSuspended()) { User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]); } $wallet = $john->wallets()->first(); $wallet->discount()->dissociate(); $wallet->save(); Entitlement::where('cost', '>=', 5000)->delete(); $this->deleteTestGroup('group-test@kolab.org'); $this->clearMeetEntitlements(); } /** * {@inheritDoc} */ public function tearDown(): void { $john = $this->getTestUser('john@kolab.org'); $john->setSettings([ 'phone' => null, 'external_email' => 'john.doe.external@gmail.com', ]); if ($john->isSuspended()) { User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]); } $wallet = $john->wallets()->first(); $wallet->discount()->dissociate(); $wallet->save(); Entitlement::where('cost', '>=', 5000)->delete(); $this->deleteTestGroup('group-test@kolab.org'); $this->clearMeetEntitlements(); parent::tearDown(); } /** * Test user info page (unauthenticated) */ public function testUserUnauth(): void { // Test that the page requires authentication $this->browse(function (Browser $browser) { $jack = $this->getTestUser('jack@kolab.org'); $browser->visit('/user/' . $jack->id)->on(new Home()); }); } /** * Test user info page */ public function testUserInfo(): void { $this->browse(function (Browser $browser) { $jack = $this->getTestUser('jack@kolab.org'); $page = new UserPage($jack->id); $browser->visit(new Home()) ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true) ->on(new Dashboard()) ->visit($page) ->on($page); // Assert main info box content $browser->assertSeeIn('@user-info .card-title', $jack->email) ->with('@user-info form', function (Browser $browser) use ($jack) { $browser->assertElementsCount('.row', 7) ->assertSeeIn('.row:nth-child(1) label', 'Managed by') ->assertSeeIn('.row:nth-child(1) #manager a', 'john@kolab.org') ->assertSeeIn('.row:nth-child(2) label', 'ID (Created)') ->assertSeeIn('.row:nth-child(2) #userid', "{$jack->id} ({$jack->created_at})") ->assertSeeIn('.row:nth-child(3) label', 'Status') ->assertSeeIn('.row:nth-child(3) #status span.text-success', 'Active') ->assertSeeIn('.row:nth-child(4) label', 'First Name') ->assertSeeIn('.row:nth-child(4) #first_name', 'Jack') ->assertSeeIn('.row:nth-child(5) label', 'Last Name') ->assertSeeIn('.row:nth-child(5) #last_name', 'Daniels') ->assertSeeIn('.row:nth-child(6) label', 'External Email') ->assertMissing('.row:nth-child(6) #external_email a') ->assertSeeIn('.row:nth-child(7) label', 'Country') ->assertSeeIn('.row:nth-child(7) #country', 'United States'); }); // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 6); + ->assertElementsCount('@nav a', 7); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)') ->click('@nav #tab-aliases') ->whenAvailable('@user-aliases', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 1) ->assertSeeIn('table tbody tr:first-child td:first-child', 'jack.daniels@kolab.org') ->assertMissing('table tfoot'); }); // Assert Subscriptions tab $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)') ->click('@nav #tab-subscriptions') ->with('@user-subscriptions', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 3) ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,44 CHF') ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB') ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF') ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF') ->assertMissing('table tfoot') ->assertMissing('#reset2fa'); }); // Assert Domains tab $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)') ->click('@nav #tab-domains') ->with('@user-domains', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.'); }); // Assert Users tab $browser->assertSeeIn('@nav #tab-users', 'Users (0)') ->click('@nav #tab-users') ->with('@user-users', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no users in this account.'); }); // Assert Distribution lists tab $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (0)') ->click('@nav #tab-distlists') ->with('@user-distlists', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no distribution lists in this account.'); + }); // Assert Settings tab $browser->assertSeeIn('@nav #tab-settings', 'Settings') ->click('@nav #tab-settings') ->whenAvailable('@user-settings form', function (Browser $browser) { $browser->assertElementsCount('.row', 1) ->assertSeeIn('.row:first-child label', 'Greylisting') ->assertSeeIn('.row:first-child .text-success', 'enabled'); }); }); } /** * Test user info page (continue) * * @depends testUserInfo */ public function testUserInfo2(): void { $this->browse(function (Browser $browser) { $john = $this->getTestUser('john@kolab.org'); $page = new UserPage($john->id); $discount = Discount::where('code', 'TEST')->first(); $wallet = $john->wallet(); $wallet->discount()->associate($discount); $wallet->debit(2010); $wallet->save(); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); $john->setSetting('greylisting', null); // Click the managed-by link on Jack's page $browser->click('@user-info #manager a') ->on($page); // Assert main info box content $browser->assertSeeIn('@user-info .card-title', $john->email) ->with('@user-info form', function (Browser $browser) use ($john) { $ext_email = $john->getSetting('external_email'); $browser->assertElementsCount('.row', 9) ->assertSeeIn('.row:nth-child(1) label', 'ID (Created)') ->assertSeeIn('.row:nth-child(1) #userid', "{$john->id} ({$john->created_at})") ->assertSeeIn('.row:nth-child(2) label', 'Status') ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active') ->assertSeeIn('.row:nth-child(3) label', 'First Name') ->assertSeeIn('.row:nth-child(3) #first_name', 'John') ->assertSeeIn('.row:nth-child(4) label', 'Last Name') ->assertSeeIn('.row:nth-child(4) #last_name', 'Doe') ->assertSeeIn('.row:nth-child(5) label', 'Organization') ->assertSeeIn('.row:nth-child(5) #organization', 'Kolab Developers') ->assertSeeIn('.row:nth-child(6) label', 'Phone') ->assertSeeIn('.row:nth-child(6) #phone', $john->getSetting('phone')) ->assertSeeIn('.row:nth-child(7) label', 'External Email') ->assertSeeIn('.row:nth-child(7) #external_email a', $ext_email) ->assertAttribute('.row:nth-child(7) #external_email a', 'href', "mailto:$ext_email") ->assertSeeIn('.row:nth-child(8) label', 'Address') ->assertSeeIn('.row:nth-child(8) #billing_address', $john->getSetting('billing_address')) ->assertSeeIn('.row:nth-child(9) label', 'Country') ->assertSeeIn('.row:nth-child(9) #country', 'United States'); }); // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 6); + ->assertElementsCount('@nav a', 7); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)') ->click('@nav #tab-aliases') ->whenAvailable('@user-aliases', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 1) ->assertSeeIn('table tbody tr:first-child td:first-child', 'john.doe@kolab.org') ->assertMissing('table tfoot'); }); // Assert Subscriptions tab $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)') ->click('@nav #tab-subscriptions') ->with('@user-subscriptions', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 3) ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB') ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') ->assertMissing('table tfoot') ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher'); }); // Assert Domains tab $browser->assertSeeIn('@nav #tab-domains', 'Domains (1)') ->click('@nav #tab-domains') ->with('@user-domains table', function (Browser $browser) { $browser->assertElementsCount('tbody tr', 1) ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'kolab.org') ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success') ->assertMissing('tfoot'); }); // Assert Distribution lists tab $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (1)') ->click('@nav #tab-distlists') ->with('@user-distlists table', function (Browser $browser) { $browser->assertElementsCount('tbody tr', 1) ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'group-test@kolab.org') ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-danger') ->assertMissing('tfoot'); }); // Assert Users tab $browser->assertSeeIn('@nav #tab-users', 'Users (4)') ->click('@nav #tab-users') ->with('@user-users table', function (Browser $browser) { $browser->assertElementsCount('tbody tr', 4) ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'jack@kolab.org') ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success') ->assertSeeIn('tbody tr:nth-child(2) td:first-child a', 'joe@kolab.org') ->assertVisible('tbody tr:nth-child(2) td:first-child svg.text-success') ->assertSeeIn('tbody tr:nth-child(3) td:first-child span', 'john@kolab.org') ->assertVisible('tbody tr:nth-child(3) td:first-child svg.text-success') ->assertSeeIn('tbody tr:nth-child(4) td:first-child a', 'ned@kolab.org') ->assertVisible('tbody tr:nth-child(4) td:first-child svg.text-success') ->assertMissing('tfoot'); }); }); // Now we go to Ned's info page, he's a controller on John's wallet $this->browse(function (Browser $browser) { $ned = $this->getTestUser('ned@kolab.org'); $beta_sku = Sku::where('title', 'beta')->first(); $storage_sku = Sku::where('title', 'storage')->first(); $wallet = $ned->wallet(); // Add an extra storage and beta entitlement with different prices Entitlement::create([ 'wallet_id' => $wallet->id, 'sku_id' => $beta_sku->id, 'cost' => 5010, 'entitleable_id' => $ned->id, 'entitleable_type' => User::class ]); Entitlement::create([ 'wallet_id' => $wallet->id, 'sku_id' => $storage_sku->id, 'cost' => 5000, 'entitleable_id' => $ned->id, 'entitleable_type' => User::class ]); $page = new UserPage($ned->id); $ned->setSetting('greylisting', 'false'); $browser->click('@user-users tbody tr:nth-child(4) td:first-child a') ->on($page); // Assert main info box content $browser->assertSeeIn('@user-info .card-title', $ned->email) ->with('@user-info form', function (Browser $browser) use ($ned) { $browser->assertSeeIn('.row:nth-child(2) label', 'ID (Created)') ->assertSeeIn('.row:nth-child(2) #userid', "{$ned->id} ({$ned->created_at})"); }); // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 6); + ->assertElementsCount('@nav a', 7); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (0)') ->click('@nav #tab-aliases') ->whenAvailable('@user-aliases', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'This user has no email aliases.'); }); // Assert Subscriptions tab, we expect John's discount here $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (6)') ->click('@nav #tab-subscriptions') ->with('@user-subscriptions', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 6) ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 3 GB') ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '45,00 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', 'Activesync') ->assertSeeIn('table tbody tr:nth-child(4) td:last-child', '0,90 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication') ->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(6) td:first-child', 'Private Beta (invitation only)') ->assertSeeIn('table tbody tr:nth-child(6) td:last-child', '45,09 CHF/month¹') ->assertMissing('table tfoot') ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher') ->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth'); }); // We don't expect John's domains here $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)') ->click('@nav #tab-domains') ->with('@user-domains', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.'); }); // We don't expect John's users here $browser->assertSeeIn('@nav #tab-users', 'Users (0)') ->click('@nav #tab-users') ->with('@user-users', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no users in this account.'); }); // We don't expect John's distribution lists here $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (0)') ->click('@nav #tab-distlists') ->with('@user-distlists', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no distribution lists in this account.'); + }); // Assert Settings tab $browser->assertSeeIn('@nav #tab-settings', 'Settings') ->click('@nav #tab-settings') ->whenAvailable('@user-settings form', function (Browser $browser) { $browser->assertElementsCount('.row', 1) ->assertSeeIn('.row:first-child label', 'Greylisting') ->assertSeeIn('.row:first-child .text-danger', 'disabled'); }); }); } /** * Test editing an external email * * @depends testUserInfo2 */ public function testExternalEmail(): void { $this->browse(function (Browser $browser) { $john = $this->getTestUser('john@kolab.org'); $browser->visit(new UserPage($john->id)) ->waitFor('@user-info #external_email button') ->click('@user-info #external_email button') // Test dialog content, and closing it with Cancel button ->with(new Dialog('#email-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'External Email') ->assertFocused('@body input') ->assertValue('@body input', 'john.doe.external@gmail.com') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Submit') ->click('@button-cancel'); }) ->assertMissing('#email-dialog') ->click('@user-info #external_email button') // Test email validation error handling, and email update ->with(new Dialog('#email-dialog'), function (Browser $browser) { $browser->type('@body input', 'test') ->click('@button-action') ->waitFor('@body input.is-invalid') ->assertSeeIn( '@body input + .invalid-feedback', 'The external email must be a valid email address.' ) ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->type('@body input', 'test@test.com') ->click('@button-action'); }) ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.') ->assertSeeIn('@user-info #external_email a', 'test@test.com') ->click('@user-info #external_email button') ->with(new Dialog('#email-dialog'), function (Browser $browser) { $browser->assertValue('@body input', 'test@test.com') ->assertMissing('@body input.is-invalid') ->assertMissing('@body input + .invalid-feedback') ->click('@button-cancel'); }) ->assertSeeIn('@user-info #external_email a', 'test@test.com'); // $john->getSetting() may not work here as it uses internal cache // read the value form database $current_ext_email = $john->settings()->where('key', 'external_email')->first()->value; $this->assertSame('test@test.com', $current_ext_email); }); } /** * Test suspending/unsuspending the user */ public function testSuspendAndUnsuspend(): void { $this->browse(function (Browser $browser) { $john = $this->getTestUser('john@kolab.org'); $browser->visit(new UserPage($john->id)) ->assertVisible('@user-info #button-suspend') ->assertMissing('@user-info #button-unsuspend') ->click('@user-info #button-suspend') ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.') ->assertSeeIn('@user-info #status span.text-warning', 'Suspended') ->assertMissing('@user-info #button-suspend') ->click('@user-info #button-unsuspend') ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.') ->assertSeeIn('@user-info #status span.text-success', 'Active') ->assertVisible('@user-info #button-suspend') ->assertMissing('@user-info #button-unsuspend'); }); } /** * Test resetting 2FA for the user */ public function testReset2FA(): void { $this->browse(function (Browser $browser) { $this->deleteTestUser('userstest1@kolabnow.com'); $user = $this->getTestUser('userstest1@kolabnow.com'); $sku2fa = Sku::firstOrCreate(['title' => '2fa']); $user->assignSku($sku2fa); SecondFactor::seed('userstest1@kolabnow.com'); $browser->visit(new UserPage($user->id)) ->click('@nav #tab-subscriptions') ->with('@user-subscriptions', function (Browser $browser) use ($sku2fa) { $browser->waitFor('#reset2fa') ->assertVisible('#sku' . $sku2fa->id); }) ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)') ->click('#reset2fa') ->with(new Dialog('#reset-2fa-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', '2-Factor Authentication Reset') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Reset') ->click('@button-action'); }) ->assertToast(Toast::TYPE_SUCCESS, '2-Factor authentication reset successfully.') ->assertMissing('#sku' . $sku2fa->id) ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)'); }); } } diff --git a/src/tests/Browser/Reseller/DomainTest.php b/src/tests/Browser/Reseller/DomainTest.php index 405456bc..2da30598 100644 --- a/src/tests/Browser/Reseller/DomainTest.php +++ b/src/tests/Browser/Reseller/DomainTest.php @@ -1,120 +1,120 @@ browse(function (Browser $browser) { $domain = $this->getTestDomain('kolab.org'); $browser->visit('/domain/' . $domain->id)->on(new Home()); }); } /** * Test domain info page */ public function testDomainInfo(): void { $this->browse(function (Browser $browser) { $domain = $this->getTestDomain('kolab.org'); $domain_page = new DomainPage($domain->id); $reseller = $this->getTestUser('reseller@kolabnow.com'); $user = $this->getTestUser('john@kolab.org'); $user_page = new UserPage($user->id); // Goto the domain page $browser->visit(new Home()) ->submitLogon('reseller@kolabnow.com', 'reseller', true) ->on(new Dashboard()) ->visit($user_page) ->on($user_page) ->click('@nav #tab-domains') ->pause(1000) ->click('@user-domains table tbody tr:first-child td a'); $browser->on($domain_page) ->assertSeeIn('@domain-info .card-title', 'kolab.org') ->with('@domain-info form', function (Browser $browser) use ($domain) { $browser->assertElementsCount('.row', 2) ->assertSeeIn('.row:nth-child(1) label', 'ID (Created)') ->assertSeeIn('.row:nth-child(1) #domainid', "{$domain->id} ({$domain->created_at})") ->assertSeeIn('.row:nth-child(2) label', 'Status') ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active'); }); // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 1); + ->assertElementsCount('@nav a', 2); // Assert Configuration tab $browser->assertSeeIn('@nav #tab-config', 'Configuration') ->with('@domain-config', function (Browser $browser) { $browser->assertSeeIn('pre#dns-verify', 'kolab-verify.kolab.org.') ->assertSeeIn('pre#dns-config', 'kolab.org.'); }); }); } /** * Test suspending/unsuspending a domain * * @depends testDomainInfo */ public function testSuspendAndUnsuspend(): void { $this->browse(function (Browser $browser) { $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY | Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED, 'type' => Domain::TYPE_EXTERNAL, ]); $browser->visit(new DomainPage($domain->id)) ->assertVisible('@domain-info #button-suspend') ->assertMissing('@domain-info #button-unsuspend') ->click('@domain-info #button-suspend') ->assertToast(Toast::TYPE_SUCCESS, 'Domain suspended successfully.') ->assertSeeIn('@domain-info #status span.text-warning', 'Suspended') ->assertMissing('@domain-info #button-suspend') ->click('@domain-info #button-unsuspend') ->assertToast(Toast::TYPE_SUCCESS, 'Domain unsuspended successfully.') ->assertSeeIn('@domain-info #status span.text-success', 'Active') ->assertVisible('@domain-info #button-suspend') ->assertMissing('@domain-info #button-unsuspend'); }); } } diff --git a/src/tests/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php index 6eb7b4f8..37ceb0bd 100644 --- a/src/tests/Browser/Reseller/UserTest.php +++ b/src/tests/Browser/Reseller/UserTest.php @@ -1,473 +1,473 @@ getTestUser('john@kolab.org'); $john->setSettings([ 'phone' => '+48123123123', 'external_email' => 'john.doe.external@gmail.com', ]); if ($john->isSuspended()) { User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]); } $wallet = $john->wallets()->first(); $wallet->discount()->dissociate(); $wallet->save(); $this->deleteTestGroup('group-test@kolab.org'); $this->clearMeetEntitlements(); } /** * {@inheritDoc} */ public function tearDown(): void { $john = $this->getTestUser('john@kolab.org'); $john->setSettings([ 'phone' => null, 'external_email' => 'john.doe.external@gmail.com', ]); if ($john->isSuspended()) { User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]); } $wallet = $john->wallets()->first(); $wallet->discount()->dissociate(); $wallet->save(); $this->deleteTestGroup('group-test@kolab.org'); $this->clearMeetEntitlements(); parent::tearDown(); } /** * Test user info page (unauthenticated) */ public function testUserUnauth(): void { // Test that the page requires authentication $this->browse(function (Browser $browser) { $jack = $this->getTestUser('jack@kolab.org'); $browser->visit('/user/' . $jack->id)->on(new Home()); }); } /** * Test user info page */ public function testUserInfo(): void { $this->browse(function (Browser $browser) { $jack = $this->getTestUser('jack@kolab.org'); $page = new UserPage($jack->id); $browser->visit(new Home()) ->submitLogon('reseller@kolabnow.com', 'reseller', true) ->on(new Dashboard()) ->visit($page) ->on($page); // Assert main info box content $browser->assertSeeIn('@user-info .card-title', $jack->email) ->with('@user-info form', function (Browser $browser) use ($jack) { $browser->assertElementsCount('.row', 7) ->assertSeeIn('.row:nth-child(1) label', 'Managed by') ->assertSeeIn('.row:nth-child(1) #manager a', 'john@kolab.org') ->assertSeeIn('.row:nth-child(2) label', 'ID (Created)') ->assertSeeIn('.row:nth-child(2) #userid', "{$jack->id} ({$jack->created_at})") ->assertSeeIn('.row:nth-child(3) label', 'Status') ->assertSeeIn('.row:nth-child(3) #status span.text-success', 'Active') ->assertSeeIn('.row:nth-child(4) label', 'First Name') ->assertSeeIn('.row:nth-child(4) #first_name', 'Jack') ->assertSeeIn('.row:nth-child(5) label', 'Last Name') ->assertSeeIn('.row:nth-child(5) #last_name', 'Daniels') ->assertSeeIn('.row:nth-child(6) label', 'External Email') ->assertMissing('.row:nth-child(6) #external_email a') ->assertSeeIn('.row:nth-child(7) label', 'Country') ->assertSeeIn('.row:nth-child(7) #country', 'United States'); }); // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 6); + ->assertElementsCount('@nav a', 7); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)') ->click('@nav #tab-aliases') ->whenAvailable('@user-aliases', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 1) ->assertSeeIn('table tbody tr:first-child td:first-child', 'jack.daniels@kolab.org') ->assertMissing('table tfoot'); }); // Assert Subscriptions tab $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)') ->click('@nav #tab-subscriptions') ->with('@user-subscriptions', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 3) ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,44 CHF') ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB') ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF') ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF') ->assertMissing('table tfoot') ->assertMissing('#reset2fa'); }); // Assert Domains tab $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)') ->click('@nav #tab-domains') ->with('@user-domains', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.'); }); // Assert Users tab $browser->assertSeeIn('@nav #tab-users', 'Users (0)') ->click('@nav #tab-users') ->with('@user-users', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no users in this account.'); }); // Assert Distribution lists tab $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (0)') ->click('@nav #tab-distlists') ->with('@user-distlists', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no distribution lists in this account.'); }); }); } /** * Test user info page (continue) * * @depends testUserInfo */ public function testUserInfo2(): void { $this->browse(function (Browser $browser) { $john = $this->getTestUser('john@kolab.org'); $page = new UserPage($john->id); $discount = Discount::where('code', 'TEST')->first(); $wallet = $john->wallet(); $wallet->discount()->associate($discount); $wallet->debit(2010); $wallet->save(); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Click the managed-by link on Jack's page $browser->click('@user-info #manager a') ->on($page); // Assert main info box content $browser->assertSeeIn('@user-info .card-title', $john->email) ->with('@user-info form', function (Browser $browser) use ($john) { $ext_email = $john->getSetting('external_email'); $browser->assertElementsCount('.row', 9) ->assertSeeIn('.row:nth-child(1) label', 'ID (Created)') ->assertSeeIn('.row:nth-child(1) #userid', "{$john->id} ({$john->created_at})") ->assertSeeIn('.row:nth-child(2) label', 'Status') ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active') ->assertSeeIn('.row:nth-child(3) label', 'First Name') ->assertSeeIn('.row:nth-child(3) #first_name', 'John') ->assertSeeIn('.row:nth-child(4) label', 'Last Name') ->assertSeeIn('.row:nth-child(4) #last_name', 'Doe') ->assertSeeIn('.row:nth-child(5) label', 'Organization') ->assertSeeIn('.row:nth-child(5) #organization', 'Kolab Developers') ->assertSeeIn('.row:nth-child(6) label', 'Phone') ->assertSeeIn('.row:nth-child(6) #phone', $john->getSetting('phone')) ->assertSeeIn('.row:nth-child(7) label', 'External Email') ->assertSeeIn('.row:nth-child(7) #external_email a', $ext_email) ->assertAttribute('.row:nth-child(7) #external_email a', 'href', "mailto:$ext_email") ->assertSeeIn('.row:nth-child(8) label', 'Address') ->assertSeeIn('.row:nth-child(8) #billing_address', $john->getSetting('billing_address')) ->assertSeeIn('.row:nth-child(9) label', 'Country') ->assertSeeIn('.row:nth-child(9) #country', 'United States'); }); // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 6); + ->assertElementsCount('@nav a', 7); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)') ->click('@nav #tab-aliases') ->whenAvailable('@user-aliases', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 1) ->assertSeeIn('table tbody tr:first-child td:first-child', 'john.doe@kolab.org') ->assertMissing('table tfoot'); }); // Assert Subscriptions tab $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)') ->click('@nav #tab-subscriptions') ->with('@user-subscriptions', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 3) ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB') ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') ->assertMissing('table tfoot') ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher'); }); // Assert Domains tab $browser->assertSeeIn('@nav #tab-domains', 'Domains (1)') ->click('@nav #tab-domains') ->with('@user-domains table', function (Browser $browser) { $browser->assertElementsCount('tbody tr', 1) ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'kolab.org') ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success') ->assertMissing('tfoot'); }); // Assert Distribution lists tab $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (1)') ->click('@nav #tab-distlists') ->with('@user-distlists table', function (Browser $browser) { $browser->assertElementsCount('tbody tr', 1) ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'group-test@kolab.org') ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-danger') ->assertMissing('tfoot'); }); // Assert Users tab $browser->assertSeeIn('@nav #tab-users', 'Users (4)') ->click('@nav #tab-users') ->with('@user-users table', function (Browser $browser) { $browser->assertElementsCount('tbody tr', 4) ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'jack@kolab.org') ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success') ->assertSeeIn('tbody tr:nth-child(2) td:first-child a', 'joe@kolab.org') ->assertVisible('tbody tr:nth-child(2) td:first-child svg.text-success') ->assertSeeIn('tbody tr:nth-child(3) td:first-child span', 'john@kolab.org') ->assertVisible('tbody tr:nth-child(3) td:first-child svg.text-success') ->assertSeeIn('tbody tr:nth-child(4) td:first-child a', 'ned@kolab.org') ->assertVisible('tbody tr:nth-child(4) td:first-child svg.text-success') ->assertMissing('tfoot'); }); }); // Now we go to Ned's info page, he's a controller on John's wallet $this->browse(function (Browser $browser) { $ned = $this->getTestUser('ned@kolab.org'); $page = new UserPage($ned->id); $browser->click('@user-users tbody tr:nth-child(4) td:first-child a') ->on($page); // Assert main info box content $browser->assertSeeIn('@user-info .card-title', $ned->email) ->with('@user-info form', function (Browser $browser) use ($ned) { $browser->assertSeeIn('.row:nth-child(2) label', 'ID (Created)') ->assertSeeIn('.row:nth-child(2) #userid', "{$ned->id} ({$ned->created_at})"); }); // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 6); + ->assertElementsCount('@nav a', 7); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (0)') ->click('@nav #tab-aliases') ->whenAvailable('@user-aliases', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'This user has no email aliases.'); }); // Assert Subscriptions tab, we expect John's discount here $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (5)') ->click('@nav #tab-subscriptions') ->with('@user-subscriptions', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 5) ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB') ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', 'Activesync') ->assertSeeIn('table tbody tr:nth-child(4) td:last-child', '0,90 CHF/month¹') ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication') ->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹') ->assertMissing('table tfoot') ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher') ->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth'); }); // We don't expect John's domains here $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)') ->click('@nav #tab-domains') ->with('@user-domains', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.'); }); // We don't expect John's users here $browser->assertSeeIn('@nav #tab-users', 'Users (0)') ->click('@nav #tab-users') ->with('@user-users', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no users in this account.'); }); // We don't expect John's distribution lists here $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (0)') ->click('@nav #tab-distlists') ->with('@user-distlists', function (Browser $browser) { $browser->assertElementsCount('table tbody tr', 0) ->assertSeeIn('table tfoot tr td', 'There are no distribution lists in this account.'); }); }); } /** * Test editing an external email * * @depends testUserInfo2 */ public function testExternalEmail(): void { $this->browse(function (Browser $browser) { $john = $this->getTestUser('john@kolab.org'); $browser->visit(new UserPage($john->id)) ->waitFor('@user-info #external_email button') ->click('@user-info #external_email button') // Test dialog content, and closing it with Cancel button ->with(new Dialog('#email-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'External Email') ->assertFocused('@body input') ->assertValue('@body input', 'john.doe.external@gmail.com') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Submit') ->click('@button-cancel'); }) ->assertMissing('#email-dialog') ->click('@user-info #external_email button') // Test email validation error handling, and email update ->with(new Dialog('#email-dialog'), function (Browser $browser) { $browser->type('@body input', 'test') ->click('@button-action') ->waitFor('@body input.is-invalid') ->assertSeeIn( '@body input + .invalid-feedback', 'The external email must be a valid email address.' ) ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->type('@body input', 'test@test.com') ->click('@button-action'); }) ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.') ->assertSeeIn('@user-info #external_email a', 'test@test.com') ->click('@user-info #external_email button') ->with(new Dialog('#email-dialog'), function (Browser $browser) { $browser->assertValue('@body input', 'test@test.com') ->assertMissing('@body input.is-invalid') ->assertMissing('@body input + .invalid-feedback') ->click('@button-cancel'); }) ->assertSeeIn('@user-info #external_email a', 'test@test.com'); // $john->getSetting() may not work here as it uses internal cache // read the value form database $current_ext_email = $john->settings()->where('key', 'external_email')->first()->value; $this->assertSame('test@test.com', $current_ext_email); }); } /** * Test suspending/unsuspending the user */ public function testSuspendAndUnsuspend(): void { $this->browse(function (Browser $browser) { $john = $this->getTestUser('john@kolab.org'); $browser->visit(new UserPage($john->id)) ->assertVisible('@user-info #button-suspend') ->assertMissing('@user-info #button-unsuspend') ->click('@user-info #button-suspend') ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.') ->assertSeeIn('@user-info #status span.text-warning', 'Suspended') ->assertMissing('@user-info #button-suspend') ->click('@user-info #button-unsuspend') ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.') ->assertSeeIn('@user-info #status span.text-success', 'Active') ->assertVisible('@user-info #button-suspend') ->assertMissing('@user-info #button-unsuspend'); }); } /** * Test resetting 2FA for the user */ public function testReset2FA(): void { $this->browse(function (Browser $browser) { $this->deleteTestUser('userstest1@kolabnow.com'); $user = $this->getTestUser('userstest1@kolabnow.com'); $sku2fa = Sku::firstOrCreate(['title' => '2fa']); $user->assignSku($sku2fa); SecondFactor::seed('userstest1@kolabnow.com'); $browser->visit(new UserPage($user->id)) ->click('@nav #tab-subscriptions') ->with('@user-subscriptions', function (Browser $browser) use ($sku2fa) { $browser->waitFor('#reset2fa') ->assertVisible('#sku' . $sku2fa->id); }) ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)') ->click('#reset2fa') ->with(new Dialog('#reset-2fa-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', '2-Factor Authentication Reset') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Reset') ->click('@button-action'); }) ->assertToast(Toast::TYPE_SUCCESS, '2-Factor authentication reset successfully.') ->assertMissing('#sku' . $sku2fa->id) ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)'); }); } } diff --git a/src/tests/Browser/StatusTest.php b/src/tests/Browser/StatusTest.php index 781e2e76..d49e1bbd 100644 --- a/src/tests/Browser/StatusTest.php +++ b/src/tests/Browser/StatusTest.php @@ -1,289 +1,289 @@ first(); if ($domain->isConfirmed()) { $domain->status ^= Domain::STATUS_CONFIRMED; $domain->save(); } $john = $this->getTestUser('john@kolab.org'); $john->created_at = Carbon::now(); if ($john->isImapReady()) { $john->status ^= User::STATUS_IMAP_READY; } $john->save(); $this->browse(function ($browser) use ($john, $domain) { $browser->visit(new Home()) ->submitLogon('john@kolab.org', 'simple123', true) ->on(new Dashboard()) ->with(new Status(), function ($browser) use ($john) { $browser->assertSeeIn('@body', 'We are preparing your account') ->assertProgress(71, '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', '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) { $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(); // This should take less than 10 seconds $browser->waitUntilMissing('@status', 10); }); // Test the Refresh button if ($domain->isConfirmed()) { $domain->status ^= Domain::STATUS_CONFIRMED; $domain->save(); } $john->created_at = Carbon::now()->subSeconds(3600); if ($john->isImapReady()) { $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(71, 'Creating a mailbox...', 'failed') ->assertVisible('@refresh-button') ->assertVisible('@refresh-text'); if ($john->refresh()->isImapReady()) { $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'); }); } /** * Test domain status on domains list and domain info page * * @depends testDashboard */ public function testDomainStatus(): void { $domain = Domain::where('namespace', 'kolab.org')->first(); $domain->created_at = Carbon::now(); $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; $domain->save(); // side-step $this->assertFalse($domain->isNew()); $this->assertTrue($domain->isActive()); $this->assertTrue($domain->isLdapReady()); $this->assertTrue($domain->isExternal()); $this->assertFalse($domain->isHosted()); $this->assertFalse($domain->isConfirmed()); $this->assertFalse($domain->isVerified()); $this->assertFalse($domain->isSuspended()); $this->assertFalse($domain->isDeleted()); $this->browse(function ($browser) use ($domain) { // Test auto-refresh $browser->on(new Dashboard()) ->click('@links a.link-domains') ->on(new DomainList()) ->waitFor('@table tbody tr') // Assert domain status icon ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-globe.text-danger') ->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()) ->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'); }); $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); // Test Verify button $browser->click('@status #status-verify') ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.') ->waitUntilMissing('@status') - ->assertMissing('@verify') + ->waitUntilMissing('@verify') ->assertVisible('@config'); }); } /** * 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(); if ($john->isImapReady()) { $john->status ^= User::STATUS_IMAP_READY; } $john->save(); $domain = Domain::where('namespace', 'kolab.org')->first(); if ($domain->isConfirmed()) { $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()) ->waitFor('@table tbody tr') // Assert user status icons ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-user.text-success') ->assertText('@table tbody tr:first-child td:first-child svg title', 'Active') ->assertVisible('@table tbody tr:nth-child(3) td:first-child svg.fa-user.text-danger') ->assertText('@table tbody tr:nth-child(3) td:first-child svg title', 'Not Ready') ->click('@table tbody tr:nth-child(3) td:first-child a') ->on(new UserInfo()) ->with('@form', function (Browser $browser) { // 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(71, '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(); // This should take less than 10 seconds $browser->waitUntilMissing('@status', 10); }); } } diff --git a/src/tests/Feature/Stories/GreylistTest.php b/src/tests/Feature/Stories/GreylistTest.php index ec244d82..9646c230 100644 --- a/src/tests/Feature/Stories/GreylistTest.php +++ b/src/tests/Feature/Stories/GreylistTest.php @@ -1,606 +1,609 @@ setUpTest(); + $this->useServicesUrl(); $this->instance = $this->generateInstanceId(); $this->clientAddress = '212.103.80.148'; $this->net = \App\IP4Net::getNet($this->clientAddress); DB::delete("DELETE FROM greylist_connect WHERE sender_domain = 'sender.domain';"); DB::delete("DELETE FROM greylist_settings;"); DB::delete("DELETE FROM greylist_whitelist WHERE sender_domain = 'sender.domain';"); } public function tearDown(): void { DB::delete("DELETE FROM greylist_connect WHERE sender_domain = 'sender.domain';"); DB::delete("DELETE FROM greylist_settings;"); DB::delete("DELETE FROM greylist_whitelist WHERE sender_domain = 'sender.domain';"); parent::tearDown(); } public function testWithTimestamp() { - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(7)->toString() ] ); $timestamp = $this->getObjectProperty($request, 'timestamp'); $this->assertTrue( \Carbon\Carbon::parse($timestamp, 'UTC') < \Carbon\Carbon::now() ); } public function testNoNet() { - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => '127.128.129.130', 'client_name' => 'some.mx' ] ); $this->assertTrue($request->shouldDefer()); } public function testIp6Net() { - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => '2a00:1450:400a:803::2005', 'client_name' => 'some.mx' ] ); $this->assertTrue($request->shouldDefer()); } // public function testMultiRecipientThroughAlias() {} public function testWhitelistNew() { - $whitelist = \App\Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); + $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); $this->assertNull($whitelist); for ($i = 0; $i < 5; $i++) { - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => "someone{$i}@sender.domain", 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(1) ] ); $this->assertTrue($request->shouldDefer()); } - $whitelist = \App\Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); + $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); $this->assertNotNull($whitelist); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => "someone5@sender.domain", 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(1) ] ); $this->assertFalse($request->shouldDefer()); } // public function testWhitelistedHit() {} public function testWhitelistStale() { - $whitelist = \App\Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); + $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); $this->assertNull($whitelist); for ($i = 0; $i < 5; $i++) { - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => "someone{$i}@sender.domain", 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(1) ] ); $this->assertTrue($request->shouldDefer()); } - $whitelist = \App\Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); + $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); $this->assertNotNull($whitelist); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => "someone5@sender.domain", 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(1) ] ); $this->assertFalse($request->shouldDefer()); $whitelist->updated_at = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2); $whitelist->save(['timestamps' => false]); $this->assertTrue($request->shouldDefer()); } // public function testWhitelistUpdate() {} public function testNew() { $data = [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx' ]; - $response = $this->post('/api/webhooks/greylist', $data); + $response = $this->post('/api/webhooks/policy/greylist', $data); $response->assertStatus(403); } public function testRetry() { - $connect = \App\Greylist\Connect::create( + $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); $connect->created_at = \Carbon\Carbon::now()->subMinutes(6); $connect->save(); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } public function testDomainDisabled() { - $setting = \App\Greylist\Setting::create( + $setting = Greylist\Setting::create( [ 'object_id' => $this->domainHosted->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } public function testDomainEnabled() { - $connect = \App\Greylist\Connect::create( + $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => \App\IP4Net::getNet('212.103.80.148')->id, 'net_type' => \App\IP4Net::class ] ); - $setting = \App\Greylist\Setting::create( + $setting = Greylist\Setting::create( [ 'object_id' => $this->domainHosted->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled', 'value' => 'true' ] ); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); $connect->created_at = \Carbon\Carbon::now()->subMinutes(6); $connect->save(); $this->assertFalse($request->shouldDefer()); } public function testDomainDisabledUserDisabled() { - $connect = \App\Greylist\Connect::create( + $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); - $settingDomain = \App\Greylist\Setting::create( + $settingDomain = Greylist\Setting::create( [ 'object_id' => $this->domainHosted->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); - $settingUser = \App\Greylist\Setting::create( + $settingUser = Greylist\Setting::create( [ 'object_id' => $this->domainOwner->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } public function testDomainDisabledUserEnabled() { - $connect = \App\Greylist\Connect::create( + $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); - $settingDomain = \App\Greylist\Setting::create( + $settingDomain = Greylist\Setting::create( [ 'object_id' => $this->domainHosted->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); - $settingUser = \App\Greylist\Setting::create( + $settingUser = Greylist\Setting::create( [ 'object_id' => $this->domainOwner->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'true' ] ); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); $connect->created_at = \Carbon\Carbon::now()->subMinutes(6); $connect->save(); $this->assertFalse($request->shouldDefer()); } public function testInvalidDomain() { - $connect = \App\Greylist\Connect::create( + $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => 1234, 'recipient_type' => \App\Domain::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => 'not.someone@that.exists', 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); } public function testInvalidUser() { - $connect = \App\Greylist\Connect::create( + $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => 1234, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => 'not.someone@that.exists', 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); } public function testUserDisabled() { - $connect = \App\Greylist\Connect::create( + $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); - $setting = \App\Greylist\Setting::create( + $setting = Greylist\Setting::create( [ 'object_id' => $this->domainOwner->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } public function testUserEnabled() { - $connect = \App\Greylist\Connect::create( + $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); - $setting = \App\Greylist\Setting::create( + $setting = Greylist\Setting::create( [ 'object_id' => $this->domainOwner->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'true' ] ); - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); $connect->created_at = \Carbon\Carbon::now()->subMinutes(6); $connect->save(); $this->assertFalse($request->shouldDefer()); } public function testMultipleUsersAllDisabled() { - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); foreach ($this->domainUsers as $user) { - \App\Greylist\Connect::create( + Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $user->email), 'recipient_id' => $user->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); - \App\Greylist\Setting::create( + Greylist\Setting::create( [ 'object_id' => $user->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); if ($user->email == $this->domainOwner->email) { continue; } - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $user->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } } public function testMultipleUsersAnyEnabled() { - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); foreach ($this->domainUsers as $user) { - \App\Greylist\Connect::create( + Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $user->email), 'recipient_id' => $user->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); - \App\Greylist\Setting::create( + Greylist\Setting::create( [ 'object_id' => $user->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => ($user->id == $this->jack->id) ? 'true' : 'false' ] ); if ($user->email == $this->domainOwner->email) { continue; } - $request = new \App\Greylist\Request( + $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $user->email, 'client_address' => $this->clientAddress ] ); if ($user->id == $this->jack->id) { $this->assertTrue($request->shouldDefer()); } else { $this->assertFalse($request->shouldDefer()); } } } private function generateInstanceId() { $instance = []; for ($x = 0; $x < 3; $x++) { for ($y = 0; $y < 3; $y++) { $instance[] .= substr('01234567889', rand(0, 9), 1); } } return implode('.', $instance); } } diff --git a/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php b/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php index 7462dc33..7a334baa 100644 --- a/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php +++ b/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php @@ -1,312 +1,308 @@ useServicesUrl(); - } - public function tearDown() - { - $this->useNormalUrl(); - parent::tearDown(); + $this->setUpTest(); + $this->useServicesUrl(); } public function testSenderFailv4() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-fail.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderFailv6() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-fail.kolab.org', 'client_name' => 'mx.kolabnow.com', // actually IN AAAA gmail.com. 'client_address' => '2a00:1450:400a:801::2005', 'recipient' => $this->domainOwner->email ]; $this->assertFalse(strpos(':', $data['client_address'])); $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderNone() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-none.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); } public function testSenderNoNet() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-none.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '256.0.0.1', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderPass() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-pass.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); } public function testSenderPassAll() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-passall.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); } public function testSenderPermerror() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-permerror.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderSoftfail() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-fail.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); } public function testSenderTemperror() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@spf-temperror.kolab.org', 'client_name' => 'mx.kolabnow.com', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); } public function testSenderRelayPolicyHeloExactNegative() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['the.only.acceptable.helo'])); $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyHeloExactPositive() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['helo.some.relayservice.domain'])); $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyRegexpNegative() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['/a\.domain/'])); $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyRegexpPositive() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['/relayservice\.domain/'])); $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyWildcardSubdomainNegative() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['.helo.some.relayservice.domain'])); $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->removeSetting('spf_whitelist'); } public function testSenderRelayPolicyWildcardSubdomainPositive() { $data = [ 'instance' => 'test.local.instance', 'protocol_state' => 'RCPT', 'sender' => 'sender@amazon.co.uk', 'client_name' => 'helo.some.relayservice.domain', 'client_address' => '212.103.80.148', 'recipient' => $this->domainOwner->email ]; $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(403); $this->domainOwner->setSetting('spf_whitelist', json_encode(['.some.relayservice.domain'])); $response = $this->post('/api/webhooks/policy/spf', $data); $response->assertStatus(200); $this->domainOwner->removeSetting('spf_whitelist'); } } diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php index 1dde9d96..c394ee5e 100644 --- a/src/tests/TestCase.php +++ b/src/tests/TestCase.php @@ -1,57 +1,46 @@ withoutMiddleware(ThrottleRequests::class); - } - /** * Set baseURL to the admin UI location */ protected static function useAdminUrl(): void { // This will set base URL for all tests in a file. // If we wanted to access both user and admin in one test // we can also just call post/get/whatever with full url \config(['app.url' => str_replace('//', '//admin.', \config('app.url'))]); url()->forceRootUrl(config('app.url')); } /** * Set baseURL to the reseller UI location */ protected static function useResellerUrl(): void { // This will set base URL for all tests in a file. // If we wanted to access both user and admin in one test // we can also just call post/get/whatever with full url \config(['app.url' => str_replace('//', '//reseller.', \config('app.url'))]); url()->forceRootUrl(config('app.url')); } /** * Set baseURL to the services location */ protected static function useServicesUrl(): void { // This will set base URL for all tests in a file. \config(['app.url' => str_replace('//', '//services.', \config('app.url'))]); url()->forceRootUrl(config('app.url')); } } diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php index 2f4776a2..5886a995 100644 --- a/src/tests/TestCaseTrait.php +++ b/src/tests/TestCaseTrait.php @@ -1,459 +1,472 @@ 'John', 'last_name' => 'Doe', 'organization' => 'Test Domain Owner', ]; /** * Some users for the hosted domain, ultimately including the owner. * * @var \App\User[] */ protected $domainUsers = []; /** * A specific user that is a regular user in the hosted domain. */ protected $jack; /** * A specific user that is a controller on the wallet to which the hosted domain is charged. */ protected $jane; /** * A specific user that has a second factor configured. */ protected $joe; /** * One of the domains that is available for public registration. * * @var \App\Domain */ protected $publicDomain; /** * A newly generated user in a public domain. * * @var \App\User */ protected $publicDomainUser; /** * A placeholder for a password that can be generated. * * Should be generated with `\App\Utils::generatePassphrase()`. * * @var string */ protected $userPassword; /** * Assert that the entitlements for the user match the expected list of entitlements. * * @param \App\User $user The user for which the entitlements need to be pulled. * @param array $expected An array of expected \App\SKU titles. */ protected function assertUserEntitlements($user, $expected) { // Assert the user entitlements $skus = $user->entitlements()->get() ->map(function ($ent) { return $ent->sku->title; }) ->toArray(); sort($skus); Assert::assertSame($expected, $skus); } protected function backdateEntitlements($entitlements, $targetDate) { $wallets = []; $ids = []; foreach ($entitlements as $entitlement) { $ids[] = $entitlement->id; $wallets[] = $entitlement->wallet_id; } \App\Entitlement::whereIn('id', $ids)->update([ 'created_at' => $targetDate, 'updated_at' => $targetDate, ]); if (!empty($wallets)) { $wallets = array_unique($wallets); $owners = \App\Wallet::whereIn('id', $wallets)->pluck('user_id')->all(); \App\User::whereIn('id', $owners)->update(['created_at' => $targetDate]); } } /** * Removes all beta entitlements from the database */ protected function clearBetaEntitlements(): void { $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(); } /** * Creates the application. * * @return \Illuminate\Foundation\Application */ public function createApplication() { $app = require __DIR__ . '/../bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); return $app; } /** * Create a set of transaction log entries for a wallet */ protected function createTestTransactions($wallet) { $result = []; $date = Carbon::now(); $debit = 0; $entitlementTransactions = []; foreach ($wallet->entitlements as $entitlement) { if ($entitlement->cost) { $debit += $entitlement->cost; $entitlementTransactions[] = $entitlement->createTransaction( Transaction::ENTITLEMENT_BILLED, $entitlement->cost ); } } $transaction = Transaction::create( [ 'user_email' => 'jeroen@jeroen.jeroen', 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => Transaction::WALLET_DEBIT, 'amount' => $debit * -1, 'description' => 'Payment', ] ); $result[] = $transaction; Transaction::whereIn('id', $entitlementTransactions)->update(['transaction_id' => $transaction->id]); $transaction = Transaction::create( [ 'user_email' => null, 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => Transaction::WALLET_CREDIT, 'amount' => 2000, 'description' => 'Payment', ] ); $transaction->created_at = $date->next(Carbon::MONDAY); $transaction->save(); $result[] = $transaction; $types = [ Transaction::WALLET_AWARD, Transaction::WALLET_PENALTY, ]; // The page size is 10, so we generate so many to have at least two pages $loops = 10; while ($loops-- > 0) { $type = $types[count($result) % count($types)]; $transaction = Transaction::create([ 'user_email' => 'jeroen.@jeroen.jeroen', 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => $type, 'amount' => 11 * (count($result) + 1) * ($type == Transaction::WALLET_PENALTY ? -1 : 1), 'description' => 'TRANS' . $loops, ]); $transaction->created_at = $date->next(Carbon::MONDAY); $transaction->save(); $result[] = $transaction; } return $result; } /** * Delete a test domain whatever it takes. * * @coversNothing */ protected function deleteTestDomain($name) { Queue::fake(); $domain = Domain::withTrashed()->where('namespace', $name)->first(); if (!$domain) { return; } $job = new \App\Jobs\Domain\DeleteJob($domain->id); $job->handle(); $domain->forceDelete(); } /** * Delete a test group whatever it takes. * * @coversNothing */ protected function deleteTestGroup($email) { Queue::fake(); $group = Group::withTrashed()->where('email', $email)->first(); if (!$group) { return; } $job = new \App\Jobs\Group\DeleteJob($group->id); $job->handle(); $group->forceDelete(); } /** * Delete a test user whatever it takes. * * @coversNothing */ protected function deleteTestUser($email) { Queue::fake(); $user = User::withTrashed()->where('email', $email)->first(); if (!$user) { return; } $job = new \App\Jobs\User\DeleteJob($user->id); $job->handle(); $user->forceDelete(); } /** * Helper to access protected property of an object */ protected static function getObjectProperty($object, $property_name) { $reflection = new \ReflectionClass($object); $property = $reflection->getProperty($property_name); $property->setAccessible(true); return $property->getValue($object); } /** * Get Domain object by namespace, create it if needed. * Skip LDAP jobs. * * @coversNothing */ protected function getTestDomain($name, $attrib = []) { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); return Domain::firstOrCreate(['namespace' => $name], $attrib); } /** * Get Group object by email, create it if needed. * Skip LDAP jobs. */ protected function getTestGroup($email, $attrib = []) { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); return Group::firstOrCreate(['email' => $email], $attrib); } /** * Get User object by email, create it if needed. * Skip LDAP jobs. * * @coversNothing */ protected function getTestUser($email, $attrib = []) { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); $user = User::firstOrCreate(['email' => $email], $attrib); if ($user->trashed()) { // Note: we do not want to use user restore here User::where('id', $user->id)->forceDelete(); $user = User::create(['email' => $email] + $attrib); } return $user; } /** * Call protected/private method of a class. * * @param object $object Instantiated object that we will run method on. * @param string $methodName Method name to call * @param array $parameters Array of parameters to pass into method. * * @return mixed Method return. */ protected function invokeMethod($object, $methodName, array $parameters = array()) { $reflection = new \ReflectionClass(get_class($object)); $method = $reflection->getMethod($methodName); $method->setAccessible(true); return $method->invokeArgs($object, $parameters); } public function setUp(): void { parent::setUp(); + // Disable throttling + $this->withoutMiddleware(ThrottleRequests::class); + } + + protected function setUpTest() + { $this->userPassword = \App\Utils::generatePassphrase(); $this->domainHosted = $this->getTestDomain( 'test.domain', [ 'type' => \App\Domain::TYPE_EXTERNAL, 'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED ] ); $packageKolab = \App\Package::where('title', 'kolab')->first(); $this->domainOwner = $this->getTestUser('john@test.domain', ['password' => $this->userPassword]); $this->domainOwner->assignPackage($packageKolab); $this->domainOwner->setSettings($this->domainOwnerSettings); // separate for regular user $this->jack = $this->getTestUser('jack@test.domain', ['password' => $this->userPassword]); // separate for wallet controller $this->jane = $this->getTestUser('jane@test.domain', ['password' => $this->userPassword]); $this->joe = $this->getTestUser('joe@test.domain', ['password' => $this->userPassword]); $this->domainUsers[] = $this->jack; $this->domainUsers[] = $this->jane; $this->domainUsers[] = $this->joe; $this->domainUsers[] = $this->getTestUser('jill@test.domain', ['password' => $this->userPassword]); foreach ($this->domainUsers as $user) { $this->domainOwner->assignPackage($packageKolab, $user); } $this->domainUsers[] = $this->domainOwner; // assign second factor to joe $this->joe->assignSku(\App\Sku::where('title', '2fa')->first()); \App\Auth\SecondFactor::seed($this->joe->email); usort( $this->domainUsers, function ($a, $b) { return $a->email > $b->email; } ); $this->domainHosted->assignPackage( \App\Package::where('title', 'domain-hosting')->first(), $this->domainOwner ); $wallet = $this->domainOwner->wallets()->first(); $wallet->addController($this->jane); $this->publicDomain = \App\Domain::where('type', \App\Domain::TYPE_PUBLIC)->first(); $this->publicDomainUser = $this->getTestUser( 'john@' . $this->publicDomain->namespace, ['password' => $this->userPassword] ); $this->publicDomainUser->assignPackage($packageKolab); } public function tearDown(): void { foreach ($this->domainUsers as $user) { if ($user == $this->domainOwner) { continue; } $this->deleteTestUser($user->email); } - $this->deleteTestUser($this->domainOwner->email); - $this->deleteTestDomain($this->domainHosted->namespace); + if ($this->domainOwner) { + $this->deleteTestUser($this->domainOwner->email); + } - $this->deleteTestUser($this->publicDomainUser->email); + if ($this->domainHosted) { + $this->deleteTestDomain($this->domainHosted->namespace); + } + + if ($this->publicDomainUser) { + $this->deleteTestUser($this->publicDomainUser->email); + } parent::tearDown(); } }