diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php index de413278..a13078bc 100644 --- a/src/tests/Browser/DomainTest.php +++ b/src/tests/Browser/DomainTest.php @@ -1,132 +1,134 @@ browse(function ($browser) { $browser->visit('/domain/123')->on(new Home()); }); } /** * Test domain info page (non-existing domain id) */ 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 */ public function testDomainInfo(): void { $this->browse(function ($browser) { // Unconfirmed domain $domain = Domain::where('namespace', 'kolab.org')->first(); - $domain->status ^= Domain::STATUS_CONFIRMED; - $domain->save(); + if ($domain->isConfirmed()) { + $domain->status ^= Domain::STATUS_CONFIRMED; + $domain->save(); + } $browser->visit('/domain/' . $domain->id) ->on(new DomainInfo()) ->whenAvailable('@verify', function ($browser) use ($domain) { $browser->assertSeeIn('pre', $domain->namespace) ->assertSeeIn('pre', $domain->hash()) ->click('button') ->assertToast(Toast::TYPE_ERROR, 'Domain ownership verification failed.'); // Make sure the domain is confirmed now $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); $browser->click('button') ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.'); }) ->whenAvailable('@config', function ($browser) use ($domain) { $browser->assertSeeIn('pre', $domain->namespace); }) ->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 domains list page (unauthenticated) */ public function testDomainListUnauth(): void { // Test that the page requires authentication $this->browse(function ($browser) { $browser->visit('/logout') ->visit('/domains') ->on(new Home()); }); } /** * Test domains list page * * @depends testDomainListUnauth */ 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()) ->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') ->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") } } diff --git a/src/tests/Browser/PaymentMollieTest.php b/src/tests/Browser/PaymentMollieTest.php index 85be5035..5b66eefe 100644 --- a/src/tests/Browser/PaymentMollieTest.php +++ b/src/tests/Browser/PaymentMollieTest.php @@ -1,233 +1,233 @@ deleteTestUser('payment-test@kolabnow.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestUser('payment-test@kolabnow.com'); parent::tearDown(); } /** * Test the payment process * * @group mollie */ public function testPayment(): void { $user = $this->getTestUser('payment-test@kolabnow.com', [ 'password' => 'simple123', ]); $this->browse(function (Browser $browser) { $browser->visit(new Home()) ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'mollie']) ->on(new Dashboard()) ->click('@links .link-wallet') ->on(new WalletPage()) ->assertSeeIn('@main button', 'Add credit') ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Top up your wallet') ->assertFocused('#amount') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@body #payment-form button', 'Continue') // Test error handling ->type('@body #amount', 'aaa') ->click('@body #payment-form button') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->assertSeeIn('#amount + span + .invalid-feedback', 'The amount must be a number.') // Submit valid data ->type('@body #amount', '12.34') ->click('@body #payment-form button'); }) ->on(new PaymentMollie()) ->assertSeeIn('@title', \config('app.name') . ' Payment') ->assertSeeIn('@amount', 'CHF 12.34'); // Looks like the Mollie testing mode is limited. // We'll select credit card method and mark the payment as paid // We can't do much more, we have to trust Mollie their page works ;) // For some reason I don't get the method selection form, it // immediately jumps to the next step. Let's detect that if ($browser->element('@methods')) { $browser->click('@methods button.grid-button-creditcard') ->waitFor('button.form__button'); } $browser->click('@status-table input[value="paid"]') ->click('button.form__button'); // Now it should redirect back to wallet page and in background // use the webhook to update payment status (and balance). // Looks like in test-mode the webhook is executed before redirect // so we can expect balance updated on the wallet page $browser->waitForLocation('/wallet') ->on(new WalletPage()) - ->assertSeeIn('@main .card-text', 'Current account balance is 12,34 CHF'); + ->assertSeeIn('@main .card-title', 'Account balance 12,34 CHF'); }); } /** * Test the auto-payment setup process * * @group mollie */ public function testAutoPaymentSetup(): void { $user = $this->getTestUser('payment-test@kolabnow.com', [ 'password' => 'simple123', ]); $this->browse(function (Browser $browser) { $browser->visit(new Home()) ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'mollie']) ->on(new Dashboard()) ->click('@links .link-wallet') ->on(new WalletPage()) ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Top up your wallet') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@body #mandate-form button', 'Set up auto-payment') ->click('@body #mandate-form button') ->assertSeeIn('@title', 'Add auto-payment') ->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by') ->assertValue('@body #mandate_amount', PaymentProvider::MIN_AMOUNT / 100) ->assertSeeIn('@body label[for="mandate_balance"]', 'when account balance is below') // phpcs:ignore ->assertValue('@body #mandate_balance', '0') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Continue') // Test error handling ->type('@body #mandate_amount', 'aaa') ->type('@body #mandate_balance', '-1') ->click('@button-action') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->assertVisible('@body #mandate_amount.is-invalid') ->assertVisible('@body #mandate_balance.is-invalid') ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') ->assertSeeIn('#mandate_balance + span + .invalid-feedback', 'The balance must be at least 0.') ->type('@body #mandate_amount', 'aaa') ->type('@body #mandate_balance', '0') ->click('@button-action') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->assertVisible('@body #mandate_amount.is-invalid') ->assertMissing('@body #mandate_balance.is-invalid') ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') ->assertMissing('#mandate_balance + span + .invalid-feedback') // Submit valid data ->type('@body #mandate_amount', '100') ->type('@body #mandate_balance', '0') ->click('@button-action'); }) ->on(new PaymentMollie()) ->assertSeeIn('@title', \config('app.name') . ' Auto-Payment Setup') ->assertMissing('@amount') ->submitValidCreditCard() ->waitForLocation('/wallet') ->visit('/wallet?paymentProvider=mollie') ->on(new WalletPage()) ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $expected = 'Auto-payment is set to fill up your account by 100 CHF every' . ' time your account balance gets under 0 CHF. You will be charged' . ' via Mastercard (**** **** **** 6787).'; $browser->assertSeeIn('@title', 'Top up your wallet') ->waitFor('#mandate-info') ->assertSeeIn('#mandate-info p:first-child', $expected) ->click('@button-cancel'); }); }); // Test updating auto-payment $this->browse(function (Browser $browser) use ($user) { $wallet = $user->wallets()->first(); $wallet->setSetting('mandate_disabled', 1); $browser->refresh() ->on(new WalletPage()) ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->waitFor('@body #mandate-info') ->assertSeeIn( '@body #mandate-info p.disabled-mandate', 'The configured auto-payment has been disabled' ) ->assertSeeIn('@body #mandate-info button.btn-primary', 'Change auto-payment') ->click('@body #mandate-info button.btn-primary') ->assertSeeIn('@title', 'Update auto-payment') ->assertSeeIn( '@body form p.disabled-mandate', 'The auto-payment is disabled.' ) ->assertValue('@body #mandate_amount', '100') ->assertValue('@body #mandate_balance', '0') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Submit') // Test error handling ->type('@body #mandate_amount', 'aaa') ->click('@button-action') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->assertVisible('@body #mandate_amount.is-invalid') ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') // Submit valid data ->type('@body #mandate_amount', '50') ->click('@button-action'); }) ->waitUntilMissing('#payment-dialog') ->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been updated.') // Open the dialog again and make sure the "disabled" text isn't there ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertMissing('@body #mandate-info p.disabled-mandate') ->click('@body #mandate-info button.btn-primary') ->assertMissing('@body form p.disabled-mandate') ->click('@button-cancel'); }); }); // Test deleting auto-payment $this->browse(function (Browser $browser) { $browser->on(new WalletPage()) ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@body #mandate-info button.btn-danger', 'Cancel auto-payment') ->click('@body #mandate-info button.btn-danger') ->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been removed.') ->assertVisible('@body #mandate-form') ->assertMissing('@body #mandate-info'); }); }); } } diff --git a/src/tests/Browser/PaymentStripeTest.php b/src/tests/Browser/PaymentStripeTest.php index 042d2e9e..c37e47aa 100644 --- a/src/tests/Browser/PaymentStripeTest.php +++ b/src/tests/Browser/PaymentStripeTest.php @@ -1,202 +1,202 @@ deleteTestUser('payment-test@kolabnow.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestUser('payment-test@kolabnow.com'); parent::tearDown(); } /** * Test the payment process * * @group stripe */ public function testPayment(): void { $user = $this->getTestUser('payment-test@kolabnow.com', [ 'password' => 'simple123', ]); $this->browse(function (Browser $browser) use ($user) { $browser->visit(new Home()) ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'stripe']) ->on(new Dashboard()) ->click('@links .link-wallet') ->on(new WalletPage()) ->assertSeeIn('@main button', 'Add credit') ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Top up your wallet') ->assertFocused('#amount') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@body #payment-form button', 'Continue') // Test error handling ->type('@body #amount', 'aaa') ->click('@body #payment-form button') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->assertSeeIn('#amount + span + .invalid-feedback', 'The amount must be a number.') // Submit valid data ->type('@body #amount', '12.34') ->click('@body #payment-form button'); }) ->on(new PaymentStripe()) ->assertSeeIn('@title', \config('app.name') . ' Payment') ->assertSeeIn('@amount', 'CHF 12.34') ->assertValue('@email-input', $user->email) ->submitValidCreditCard(); // Now it should redirect back to wallet page and in background // use the webhook to update payment status (and balance). // Looks like in test-mode the webhook is executed before redirect // so we can expect balance updated on the wallet page $browser->waitForLocation('/wallet', 30) // need more time than default 5 sec. ->on(new WalletPage()) - ->assertSeeIn('@main .card-text', 'Current account balance is 12,34 CHF'); + ->assertSeeIn('@main .card-title', 'Account balance 12,34 CHF'); }); } /** * Test the auto-payment setup process * * @group stripe */ public function testAutoPaymentSetup(): void { $user = $this->getTestUser('payment-test@kolabnow.com', [ 'password' => 'simple123', ]); // Test creating auto-payment $this->browse(function (Browser $browser) use ($user) { $browser->visit(new Home()) ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'stripe']) ->on(new Dashboard()) ->click('@links .link-wallet') ->on(new WalletPage()) ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Top up your wallet') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@body #mandate-form button', 'Set up auto-payment') ->click('@body #mandate-form button') ->assertSeeIn('@title', 'Add auto-payment') ->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by') ->assertValue('@body #mandate_amount', PaymentProvider::MIN_AMOUNT / 100) ->assertSeeIn('@body label[for="mandate_balance"]', 'when account balance is below') // phpcs:ignore ->assertValue('@body #mandate_balance', '0') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Continue') // Test error handling ->type('@body #mandate_amount', 'aaa') ->type('@body #mandate_balance', '-1') ->click('@button-action') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->assertVisible('@body #mandate_amount.is-invalid') ->assertVisible('@body #mandate_balance.is-invalid') ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') ->assertSeeIn('#mandate_balance + span + .invalid-feedback', 'The balance must be at least 0.') ->type('@body #mandate_amount', 'aaa') ->type('@body #mandate_balance', '0') ->click('@button-action') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->assertVisible('@body #mandate_amount.is-invalid') ->assertMissing('@body #mandate_balance.is-invalid') ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') ->assertMissing('#mandate_balance + span + .invalid-feedback') // Submit valid data ->type('@body #mandate_amount', '100') ->type('@body #mandate_balance', '0') ->click('@button-action'); }) ->on(new PaymentStripe()) ->assertMissing('@title') ->assertMissing('@amount') ->assertValue('@email-input', $user->email) ->submitValidCreditCard() ->waitForLocation('/wallet', 30) // need more time than default 5 sec. ->visit('/wallet?paymentProvider=stripe') ->on(new WalletPage()) ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $expected = 'Auto-payment is set to fill up your account by 100 CHF every' . ' time your account balance gets under 0 CHF. You will be charged' . ' via Visa (**** **** **** 4242).'; $browser->assertSeeIn('@title', 'Top up your wallet') ->waitFor('#mandate-info') ->assertSeeIn('#mandate-info p:first-child', $expected) ->click('@button-cancel'); }); }); // Test updating auto-payment $this->browse(function (Browser $browser) { $browser->on(new WalletPage()) ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@body #mandate-info button.btn-primary', 'Change auto-payment') ->click('@body #mandate-info button.btn-primary') ->assertSeeIn('@title', 'Update auto-payment') ->assertValue('@body #mandate_amount', '100') ->assertValue('@body #mandate_balance', '0') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Submit') // Test error handling ->type('@body #mandate_amount', 'aaa') ->click('@button-action') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->assertVisible('@body #mandate_amount.is-invalid') ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') // Submit valid data ->type('@body #mandate_amount', '50') ->click('@button-action'); }) ->waitUntilMissing('#payment-dialog') ->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been updated.'); }); // Test deleting auto-payment $this->browse(function (Browser $browser) { $browser->on(new WalletPage()) ->click('@main button') ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@body #mandate-info button.btn-danger', 'Cancel auto-payment') ->click('@body #mandate-info button.btn-danger') ->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been removed.') ->assertVisible('@body #mandate-form') ->assertMissing('@body #mandate-info'); }); }); } } diff --git a/src/tests/Browser/StatusTest.php b/src/tests/Browser/StatusTest.php index 16a1519d..3ae338cf 100644 --- a/src/tests/Browser/StatusTest.php +++ b/src/tests/Browser/StatusTest.php @@ -1,253 +1,268 @@ first(); - $domain->status ^= Domain::STATUS_CONFIRMED; - $domain->save(); + if ($domain->isConfirmed()) { + $domain->status ^= Domain::STATUS_CONFIRMED; + $domain->save(); + } + $john = $this->getTestUser('john@kolab.org'); $john->created_at = Carbon::now(); - $john->status ^= User::STATUS_IMAP_READY; + 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(28, 'Creating a mailbox...', 'pending') ->assertMissing('#status-verify') ->assertMissing('#status-link') ->assertMissing('@refresh-button') ->assertMissing('@refresh-text'); - $john->created_at = Carbon::now(); - $john->status ^= User::STATUS_IMAP_READY; + $john->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 - $domain->status ^= Domain::STATUS_CONFIRMED; - $domain->save(); + if ($domain->isConfirmed()) { + $domain->status ^= Domain::STATUS_CONFIRMED; + $domain->save(); + } $john->created_at = Carbon::now()->subSeconds(3600); - $john->status ^= User::STATUS_IMAP_READY; + 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(28, 'Creating a mailbox...', 'failed') ->assertVisible('@refresh-button') ->assertVisible('@refresh-text'); - $john->status ^= User::STATUS_IMAP_READY; - $john->save(); + 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_CONFIRMED | Domain::STATUS_VERIFIED; + $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; $domain->save(); $this->browse(function ($browser) use ($domain) { // Test auto-refresh $browser->on(new Dashboard()) ->click('@links a.link-domains') ->on(new DomainList()) // 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') ->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(); - $john->status ^= User::STATUS_IMAP_READY; + if ($john->isImapReady()) { + $john->status ^= User::STATUS_IMAP_READY; + } $john->save(); $domain = Domain::where('namespace', 'kolab.org')->first(); - $domain->status ^= Domain::STATUS_CONFIRMED; - $domain->save(); + 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()) // 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(28, 'Creating a mailbox...', 'pending') ->assertMissing('#status-verify') ->assertMissing('#status-link') ->assertMissing('@refresh-button') ->assertMissing('@refresh-text'); - $john->status ^= User::STATUS_IMAP_READY; + + $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/Controller/Admin/WalletsTest.php b/src/tests/Feature/Controller/Admin/WalletsTest.php index 68d0782c..8759b2c4 100644 --- a/src/tests/Feature/Controller/Admin/WalletsTest.php +++ b/src/tests/Feature/Controller/Admin/WalletsTest.php @@ -1,226 +1,228 @@ 'stripe']); $user = $this->getTestUser('john@kolab.org'); $admin = $this->getTestUser('jeroen@jeroen.jeroen'); $wallet = $user->wallets()->first(); + $wallet->discount_id = null; + $wallet->save(); // Make sure there's no stripe/mollie identifiers $wallet->setSetting('stripe_id', null); $wallet->setSetting('stripe_mandate_id', null); $wallet->setSetting('mollie_id', null); $wallet->setSetting('mollie_mandate_id', null); // Non-admin user $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}"); $response->assertStatus(403); // Admin user $response = $this->actingAs($admin)->get("api/v4/wallets/{$wallet->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($wallet->id, $json['id']); $this->assertSame('CHF', $json['currency']); $this->assertSame($wallet->balance, $json['balance']); $this->assertSame(0, $json['discount']); $this->assertTrue(empty($json['description'])); $this->assertTrue(empty($json['discount_description'])); $this->assertTrue(!empty($json['provider'])); $this->assertTrue(!empty($json['providerLink'])); $this->assertTrue(!empty($json['mandate'])); } /** * Test awarding/penalizing a wallet (POST /api/v4/wallets/:id/one-off) */ public function testOneOff(): void { $user = $this->getTestUser('john@kolab.org'); $admin = $this->getTestUser('jeroen@jeroen.jeroen'); $wallet = $user->wallets()->first(); $balance = $wallet->balance; Transaction::where('object_id', $wallet->id) ->whereIn('type', [Transaction::WALLET_AWARD, Transaction::WALLET_PENALTY]) ->delete(); // Non-admin user $response = $this->actingAs($user)->post("api/v4/wallets/{$wallet->id}/one-off", []); $response->assertStatus(403); // Admin user - invalid input $post = ['amount' => 'aaaa']; $response = $this->actingAs($admin)->post("api/v4/wallets/{$wallet->id}/one-off", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame('The amount must be a number.', $json['errors']['amount'][0]); $this->assertSame('The description field is required.', $json['errors']['description'][0]); $this->assertCount(2, $json); $this->assertCount(2, $json['errors']); // Admin user - a valid bonus $post = ['amount' => '50', 'description' => 'A bonus']; $response = $this->actingAs($admin)->post("api/v4/wallets/{$wallet->id}/one-off", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The bonus has been added to the wallet successfully.', $json['message']); $this->assertSame($balance += 5000, $json['balance']); $this->assertSame($balance, $wallet->fresh()->balance); $transaction = Transaction::where('object_id', $wallet->id) ->where('type', Transaction::WALLET_AWARD)->first(); $this->assertSame($post['description'], $transaction->description); $this->assertSame(5000, $transaction->amount); $this->assertSame($admin->email, $transaction->user_email); // Admin user - a valid penalty $post = ['amount' => '-40', 'description' => 'A penalty']; $response = $this->actingAs($admin)->post("api/v4/wallets/{$wallet->id}/one-off", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The penalty has been added to the wallet successfully.', $json['message']); $this->assertSame($balance -= 4000, $json['balance']); $this->assertSame($balance, $wallet->fresh()->balance); $transaction = Transaction::where('object_id', $wallet->id) ->where('type', Transaction::WALLET_PENALTY)->first(); $this->assertSame($post['description'], $transaction->description); $this->assertSame(4000, $transaction->amount); $this->assertSame($admin->email, $transaction->user_email); } /** * Test fetching wallet transactions (GET /api/v4/wallets/:id/transactions) */ public function testTransactions(): void { // Note: Here we're testing only that the end-point works, // and admin can get the transaction log, response details // are tested in Feature/Controller/WalletsTest.php $this->deleteTestUser('wallets-controller@kolabnow.com'); $user = $this->getTestUser('wallets-controller@kolabnow.com'); $wallet = $user->wallets()->first(); $admin = $this->getTestUser('jeroen@jeroen.jeroen'); // Non-admin $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(403); // Create some sample transactions $transactions = $this->createTestTransactions($wallet); $transactions = array_reverse($transactions); $pages = array_chunk($transactions, 10 /* page size*/); // Get the 2nd page $response = $this->actingAs($admin)->get("api/v4/wallets/{$wallet->id}/transactions?page=2"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(2, $json['page']); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertCount(2, $json['list']); foreach ($pages[1] as $idx => $transaction) { $this->assertSame($transaction->id, $json['list'][$idx]['id']); $this->assertSame($transaction->type, $json['list'][$idx]['type']); $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); $this->assertFalse($json['list'][$idx]['hasDetails']); } // The 'user' key is set only on the admin end-point $this->assertSame('jeroen@jeroen.jeroen', $json['list'][1]['user']); } /** * Test updating a wallet (PUT /api/v4/wallets/:id) */ public function testUpdate(): void { $user = $this->getTestUser('john@kolab.org'); $admin = $this->getTestUser('jeroen@jeroen.jeroen'); $wallet = $user->wallets()->first(); $discount = Discount::where('code', 'TEST')->first(); // Non-admin user $response = $this->actingAs($user)->put("api/v4/wallets/{$wallet->id}", []); $response->assertStatus(403); // Admin user - setting a discount $post = ['discount' => $discount->id]; $response = $this->actingAs($admin)->put("api/v4/wallets/{$wallet->id}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('User wallet updated successfully.', $json['message']); $this->assertSame($wallet->id, $json['id']); $this->assertSame($discount->discount, $json['discount']); $this->assertSame($discount->id, $json['discount_id']); $this->assertSame($discount->description, $json['discount_description']); $this->assertSame($discount->id, $wallet->fresh()->discount->id); // Admin user - removing a discount $post = ['discount' => null]; $response = $this->actingAs($admin)->put("api/v4/wallets/{$wallet->id}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('User wallet updated successfully.', $json['message']); $this->assertSame($wallet->id, $json['id']); $this->assertSame(null, $json['discount_id']); $this->assertTrue(empty($json['discount_description'])); $this->assertSame(null, $wallet->fresh()->discount); } } diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php index 349aa570..cf2f1a31 100644 --- a/src/tests/Feature/Controller/DomainsTest.php +++ b/src/tests/Feature/Controller/DomainsTest.php @@ -1,243 +1,243 @@ deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); } public function tearDown(): void { $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); parent::tearDown(); } /** * Test domain confirm request */ public function testConfirm(): void { $sku_domain = Sku::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 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->assertSame([], $json); // 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(1, $json); $this->assertSame('kolab.org', $json[0]['namespace']); // Values below are tested by Unit tests $this->assertArrayHasKey('isConfirmed', $json[0]); $this->assertArrayHasKey('isDeleted', $json[0]); $this->assertArrayHasKey('isVerified', $json[0]); $this->assertArrayHasKey('isSuspended', $json[0]); $this->assertArrayHasKey('isActive', $json[0]); $this->assertArrayHasKey('isLdapReady', $json[0]); $response = $this->actingAs($ned)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSame('kolab.org', $json[0]['namespace']); } /** * Test fetching domain info */ public function testShow(): void { $sku_domain = Sku::where('title', 'domain-hosting')->first(); $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}"); $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->assertCount(4, $json['config']); $this->assertTrue(strpos(implode("\n", $json['config']), $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); $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 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_VERIFIED | Domain::STATUS_CONFIRMED; + $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->assertFalse($json['isReady']); $this->assertCount(4, $json['process']); $this->assertSame('domain-verified', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('domain-confirmed', $json['process'][3]['label']); $this->assertSame(false, $json['process'][3]['state']); $this->assertSame('error', $json['status']); $this->assertSame('Failed to verify an ownership of a domain.', $json['message']); // TODO: Test completing all process steps } } diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php index f6345cd4..f532fc03 100644 --- a/src/tests/Feature/Controller/PaymentsMollieTest.php +++ b/src/tests/Feature/Controller/PaymentsMollieTest.php @@ -1,587 +1,595 @@ 'mollie']); $john = $this->getTestUser('john@kolab.org'); $wallet = $john->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); Transaction::where('object_id', $wallet->id) ->where('type', Transaction::WALLET_CREDIT)->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { $john = $this->getTestUser('john@kolab.org'); $wallet = $john->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); Transaction::where('object_id', $wallet->id) ->where('type', Transaction::WALLET_CREDIT)->delete(); parent::tearDown(); } /** * Test creating/updating/deleting an outo-payment mandate * * @group mollie */ public function testMandates(): void { // Unauth access not allowed $response = $this->get("api/v4/payments/mandate"); $response->assertStatus(401); $response = $this->post("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->put("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->delete("api/v4/payments/mandate"); $response->assertStatus(401); $user = $this->getTestUser('john@kolab.org'); // Test creating a mandate (invalid input) $post = []; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); // Test creating a mandate (invalid input) $post = ['amount' => 100, 'balance' => 'a']; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame('The balance must be a number.', $json['errors']['balance'][0]); // Test creating a mandate (invalid input) $post = ['amount' => -100, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Test creating a mandate (valid input) $post = ['amount' => 20.10, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']); // Test fetching the mandate information $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertEquals('Credit Card', $json['method']); $this->assertSame(true, $json['isPending']); $this->assertSame(false, $json['isValid']); $this->assertSame(false, $json['isDisabled']); $mandate_id = $json['id']; // We would have to invoke a browser to accept the "first payment" to make // the mandate validated/completed. Instead, we'll mock the mandate object. $mollie_response = [ 'resource' => 'mandate', 'id' => $mandate_id, 'status' => 'valid', 'method' => 'creditcard', 'details' => [ 'cardNumber' => '4242', 'cardLabel' => 'Visa', ], 'customerId' => 'cst_GMfxGPt7Gj', 'createdAt' => '2020-04-28T11:09:47+00:00', ]; $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $wallet = $user->wallets()->first(); $wallet->setSetting('mandate_disabled', 1); $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertEquals('Visa (**** **** **** 4242)', $json['method']); $this->assertSame(false, $json['isPending']); $this->assertSame(true, $json['isValid']); $this->assertSame(true, $json['isDisabled']); Bus::fake(); $wallet->setSetting('mandate_disabled', null); // Test updating mandate details (invalid input) $post = []; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); $post = ['amount' => -100, 'balance' => 0]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Test updating a mandate (valid input) $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['amount' => 30.10, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertSame($mandate_id, $json['id']); $this->assertFalse($json['isDisabled']); $wallet = $user->wallets()->first(); $this->assertEquals(30.10, $wallet->getSetting('mandate_amount')); $this->assertEquals(1, $wallet->getSetting('mandate_balance')); // Test updating a disabled mandate (invalid input) $wallet->setSetting('mandate_disabled', 1); $wallet->balance = -2000; $wallet->save(); $user->refresh(); // required so the controller sees the wallet update from above $post = ['amount' => 15.10, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame('The specified amount does not cover the balance on the account.', $json['errors']['amount']); // Test updating a disabled mandate (valid input) $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['amount' => 30, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertSame($mandate_id, $json['id']); $this->assertFalse($json['isDisabled']); Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 1); Bus::assertDispatched(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { $job_wallet = $this->getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); $this->unmockMollie(); // Delete mandate $response = $this->actingAs($user)->delete("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been removed.', $json['message']); // Confirm with Mollie the mandate does not exist $customer_id = $wallet->getSetting('mollie_id'); $this->expectException(\Mollie\Api\Exceptions\ApiException::class); $this->expectExceptionMessageMatches('/410: Gone/'); $mandate = mollie()->mandates()->getForId($customer_id, $mandate_id); $this->assertNull($wallet->fresh()->getSetting('mollie_mandate_id')); } /** * Test creating a payment and receiving a status via webhook * * @group mollie */ public function testStoreAndWebhook(): void { Bus::fake(); // Unauth access not allowed $response = $this->post("api/v4/payments", []); $response->assertStatus(401); $user = $this->getTestUser('john@kolab.org'); $post = ['amount' => -1]; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); $post = ['amount' => '12.34']; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']); $wallet = $user->wallets()->first(); $payments = Payment::where('wallet_id', $wallet->id)->get(); $this->assertCount(1, $payments); $payment = $payments[0]; $this->assertSame(1234, $payment->amount); $this->assertSame(\config('app.name') . ' Payment', $payment->description); $this->assertSame('open', $payment->status); $this->assertEquals(0, $wallet->balance); // Test the webhook // Note: Webhook end-point does not require authentication $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "paid", // Status is not enough, paidAt is used to distinguish the state "paidAt" => date('c'), "mode" => "test", ]; // We'll trigger the webhook with payment id and use mocking for // a request to the Mollie payments API. We cannot force Mollie // to make the payment status change. $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['id' => $payment->id]; $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); - $transaction = $wallet->transactions()->where('type', Transaction::WALLET_CREDIT)->last(); + $transaction = $wallet->transactions() + ->where('type', Transaction::WALLET_CREDIT)->get()->last(); + $this->assertSame(1234, $transaction->amount); $this->assertSame( "Payment transaction {$payment->id} using Mollie", $transaction->description ); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); // Verify "paid -> open -> paid" scenario, assert that balance didn't change $mollie_response['status'] = 'open'; unset($mollie_response['paidAt']); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); $mollie_response['status'] = 'paid'; $mollie_response['paidAt'] = date('c'); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Test for payment failure Bus::fake(); $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "failed", "mode" => "test", ]; // We'll trigger the webhook with payment id and use mocking for // a request to the Mollie payments API. We cannot force Mollie // to make the payment status change. $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame('failed', $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); } /** * Test automatic payment charges * * @group mollie */ public function testTopUp(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Create a valid mandate first $this->createMandate($wallet, ['amount' => 20.10, 'balance' => 10]); // Expect a recurring payment as we have a valid mandate at this point $result = PaymentsController::topUpWallet($wallet); $this->assertTrue($result); // Check that the payments table contains a new record with proper amount // There should be two records, one for the first payment and another for // the recurring payment $this->assertCount(1, $wallet->payments()->get()); $payment = $wallet->payments()->first(); $this->assertSame(2010, $payment->amount); // In mollie we don't have to wait for a webhook, the response to // PaymentIntent already sets the status to 'paid', so we can test // immediately the balance update // Assert that email notification job has been dispatched $this->assertSame(PaymentProvider::STATUS_PAID, $payment->status); $this->assertEquals(2010, $wallet->fresh()->balance); - $transaction = $wallet->transactions()->where('type', Transaction::WALLET_CREDIT)->last(); + $transaction = $wallet->transactions() + ->where('type', Transaction::WALLET_CREDIT)->get()->last(); + $this->assertSame(2010, $transaction->amount); $this->assertSame( "Auto-payment transaction {$payment->id} using Mastercard (**** **** **** 6787)", $transaction->description ); Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); // Expect no payment if the mandate is disabled $wallet->setSetting('mandate_disabled', 1); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); // Expect no payment if balance is ok $wallet->setSetting('mandate_disabled', null); $wallet->balance = 1000; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); // Expect no payment if the top-up amount is not enough $wallet->setSetting('mandate_disabled', null); $wallet->balance = -2050; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) { $job_wallet = $this->getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); // Expect no payment if there's no mandate $wallet->setSetting('mollie_mandate_id', null); $wallet->balance = 0; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); // Test webhook for recurring payments + $wallet->transactions()->delete(); + $responseStack = $this->mockMollie(); Bus::fake(); $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "paid", // Status is not enough, paidAt is used to distinguish the state "paidAt" => date('c'), "mode" => "test", ]; // We'll trigger the webhook with payment id and use mocking for // a request to the Mollie payments API. We cannot force Mollie // to make the payment status change. $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['id' => $payment->id]; $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(2010, $wallet->fresh()->balance); - $transaction = $wallet->transactions()->where('type', Transaction::WALLET_CREDIT)->last(); + $transaction = $wallet->transactions() + ->where('type', Transaction::WALLET_CREDIT)->get()->last(); + $this->assertSame(2010, $transaction->amount); $this->assertSame( "Auto-payment transaction {$payment->id} using Mollie", $transaction->description ); // Assert that email notification job has been dispatched Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); Bus::fake(); // Test for payment failure $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $wallet->setSetting('mollie_mandate_id', 'xxx'); $wallet->setSetting('mandate_disabled', null); $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "failed", "mode" => "test", ]; $responseStack->append(new Response(200, [], json_encode($mollie_response))); $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $wallet->refresh(); $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); $this->assertEquals(2010, $wallet->balance); $this->assertTrue(!empty($wallet->getSetting('mandate_disabled'))); // Assert that email notification job has been dispatched Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); $responseStack = $this->unmockMollie(); } /** * Create Mollie's auto-payment mandate using our API and Chrome browser */ protected function createMandate(Wallet $wallet, array $params) { // Use the API to create a first payment with a mandate $response = $this->actingAs($wallet->owner)->post("api/v4/payments/mandate", $params); $response->assertStatus(200); $json = $response->json(); // There's no easy way to confirm a created mandate. // The only way seems to be to fire up Chrome on checkout page // and do actions with use of Dusk browser. $this->startBrowser() ->visit($json['redirectUrl']) ->click('input[value="paid"]') ->click('button.form__button'); $this->stopBrowser(); } } diff --git a/src/tests/Feature/Controller/PaymentsStripeTest.php b/src/tests/Feature/Controller/PaymentsStripeTest.php index 2fe158f8..22bebee4 100644 --- a/src/tests/Feature/Controller/PaymentsStripeTest.php +++ b/src/tests/Feature/Controller/PaymentsStripeTest.php @@ -1,670 +1,674 @@ 'stripe']); $john = $this->getTestUser('john@kolab.org'); $wallet = $john->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); Transaction::where('object_id', $wallet->id) ->where('type', Transaction::WALLET_CREDIT)->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { $john = $this->getTestUser('john@kolab.org'); $wallet = $john->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); Transaction::where('object_id', $wallet->id) ->where('type', Transaction::WALLET_CREDIT)->delete(); parent::tearDown(); } /** * Test creating/updating/deleting an outo-payment mandate * * @group stripe */ public function testMandates(): void { Bus::fake(); // Unauth access not allowed $response = $this->get("api/v4/payments/mandate"); $response->assertStatus(401); $response = $this->post("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->put("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->delete("api/v4/payments/mandate"); $response->assertStatus(401); $user = $this->getTestUser('john@kolab.org'); // Test creating a mandate (invalid input) $post = []; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); // Test creating a mandate (invalid input) $post = ['amount' => 100, 'balance' => 'a']; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame('The balance must be a number.', $json['errors']['balance'][0]); // Test creating a mandate (invalid input) $post = ['amount' => -100, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Test creating a mandate (valid input) $post = ['amount' => 20.10, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertRegExp('|^cs_test_|', $json['id']); // Test fetching the mandate information $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertSame(false, $json['isDisabled']); // We would have to invoke a browser to accept the "first payment" to make // the mandate validated/completed. Instead, we'll mock the mandate object. $setupIntent = '{ "id": "AAA", "object": "setup_intent", "created": 123456789, "payment_method": "pm_YYY", "status": "succeeded", "usage": "off_session", "customer": null }'; $paymentMethod = '{ "id": "pm_YYY", "object": "payment_method", "card": { "brand": "visa", "country": "US", "last4": "4242" }, "created": 123456789, "type": "card" }'; $client = $this->mockStripe(); $client->addResponse($setupIntent); $client->addResponse($paymentMethod); // As we do not use checkout page, we do not receive a webworker request // I.e. we have to fake the mandate id $wallet = $user->wallets()->first(); $wallet->setSetting('stripe_mandate_id', 'AAA'); $wallet->setSetting('mandate_disabled', 1); $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertEquals('Visa (**** **** **** 4242)', $json['method']); $this->assertSame(false, $json['isPending']); $this->assertSame(true, $json['isValid']); $this->assertSame(true, $json['isDisabled']); // Test updating mandate details (invalid input) $wallet->setSetting('mandate_disabled', null); $user->refresh(); $post = []; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); $post = ['amount' => -100, 'balance' => 0]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Test updating a mandate (valid input) $client->addResponse($setupIntent); $client->addResponse($paymentMethod); $post = ['amount' => 30.10, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertEquals(30.10, $wallet->getSetting('mandate_amount')); $this->assertEquals(1, $wallet->getSetting('mandate_balance')); $this->assertSame('AAA', $json['id']); $this->assertFalse($json['isDisabled']); // Test updating a disabled mandate (invalid input) $wallet->setSetting('mandate_disabled', 1); $wallet->balance = -2000; $wallet->save(); $user->refresh(); // required so the controller sees the wallet update from above $post = ['amount' => 15.10, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame('The specified amount does not cover the balance on the account.', $json['errors']['amount']); // Test updating a disabled mandate (valid input) $client->addResponse($setupIntent); $client->addResponse($paymentMethod); $post = ['amount' => 30, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertSame('AAA', $json['id']); $this->assertFalse($json['isDisabled']); Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 1); Bus::assertDispatched(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { $job_wallet = $this->getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); $this->unmockStripe(); // TODO: Delete mandate } /** * Test creating a payment and receiving a status via webhook * * @group stripe */ public function testStoreAndWebhook(): void { Bus::fake(); // Unauth access not allowed $response = $this->post("api/v4/payments", []); $response->assertStatus(401); $user = $this->getTestUser('john@kolab.org'); $post = ['amount' => -1]; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); $post = ['amount' => '12.34']; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertRegExp('|^cs_test_|', $json['id']); $wallet = $user->wallets()->first(); $payments = Payment::where('wallet_id', $wallet->id)->get(); $this->assertCount(1, $payments); $payment = $payments[0]; $this->assertSame(1234, $payment->amount); $this->assertSame(\config('app.name') . ' Payment', $payment->description); $this->assertSame('open', $payment->status); $this->assertEquals(0, $wallet->balance); // Test the webhook $post = [ 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", 'object' => "event", 'api_version' => "2020-03-02", 'created' => 1590147209, 'data' => [ 'object' => [ 'id' => $payment->id, 'object' => "payment_intent", 'amount' => 1234, 'amount_capturable' => 0, 'amount_received' => 1234, 'capture_method' => "automatic", 'client_secret' => "pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48", 'confirmation_method' => "automatic", 'created' => 1590147204, 'currency' => "chf", 'customer' => "cus_HKDZ53OsKdlM83", 'last_payment_error' => null, 'livemode' => false, 'metadata' => [], 'receipt_email' => "payment-test@kolabnow.com", 'status' => "succeeded" ] ], 'type' => "payment_intent.succeeded" ]; // Test payment succeeded event $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); - $transaction = $wallet->transactions()->where('type', Transaction::WALLET_CREDIT)->last(); + $transaction = $wallet->transactions() + ->where('type', Transaction::WALLET_CREDIT)->get()->last(); + $this->assertSame(1234, $transaction->amount); $this->assertSame( "Payment transaction {$payment->id} using Stripe", $transaction->description ); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); // Test that balance didn't change if the same event is posted $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Test for payment failure ('failed' status) $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $post['type'] = "payment_intent.payment_failed"; $post['data']['object']['status'] = 'failed'; $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); // Test for payment failure ('canceled' status) $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $post['type'] = "payment_intent.canceled"; $post['data']['object']['status'] = 'canceled'; $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_CANCELED, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); } /** * Test receiving webhook request for setup intent * * @group stripe */ public function testCreateMandateAndWebhook(): void { $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Test creating a mandate (valid input) $post = ['amount' => 20.10, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(200); $payment = $wallet->payments()->first(); $this->assertSame(PaymentProvider::STATUS_OPEN, $payment->status); $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type); $this->assertSame(0, $payment->amount); $post = [ 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", 'object' => "event", 'api_version' => "2020-03-02", 'created' => 1590147209, 'data' => [ 'object' => [ 'id' => $payment->id, 'object' => "setup_intent", 'client_secret' => "pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48", 'created' => 1590147204, 'customer' => "cus_HKDZ53OsKdlM83", 'last_setup_error' => null, 'metadata' => [], 'status' => "succeeded" ] ], 'type' => "setup_intent.succeeded" ]; // Test payment succeeded event $response = $this->webhookRequest($post); $response->assertStatus(200); $payment->refresh(); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->status); $this->assertSame($payment->id, $wallet->fresh()->getSetting('stripe_mandate_id')); // TODO: test other setup_intent.* events } /** * Test automatic payment charges * * @group stripe */ public function testTopUpAndWebhook(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Stripe API does not allow us to create a mandate easily // That's why we we'll mock API responses // Create a fake mandate $wallet->setSettings([ 'mandate_amount' => 20.10, 'mandate_balance' => 10, 'stripe_mandate_id' => 'AAA', ]); $setupIntent = json_encode([ "id" => "AAA", "object" => "setup_intent", "created" => 123456789, "payment_method" => "pm_YYY", "status" => "succeeded", "usage" => "off_session", "customer" => null ]); $paymentMethod = json_encode([ "id" => "pm_YYY", "object" => "payment_method", "card" => [ "brand" => "visa", "country" => "US", "last4" => "4242" ], "created" => 123456789, "type" => "card" ]); $paymentIntent = json_encode([ "id" => "pi_XX", "object" => "payment_intent", "created" => 123456789, "amount" => 2010, "currency" => "chf", "description" => "Kolab Recurring Payment" ]); $client = $this->mockStripe(); $client->addResponse($setupIntent); $client->addResponse($paymentMethod); $client->addResponse($setupIntent); $client->addResponse($paymentIntent); // Expect a recurring payment as we have a valid mandate at this point $result = PaymentsController::topUpWallet($wallet); $this->assertTrue($result); // Check that the payments table contains a new record with proper amount // There should be two records, one for the first payment and another for // the recurring payment $this->assertCount(1, $wallet->payments()->get()); $payment = $wallet->payments()->first(); $this->assertSame(2010, $payment->amount); $this->assertSame(\config('app.name') . " Recurring Payment", $payment->description); $this->assertSame("pi_XX", $payment->id); // Expect no payment if the mandate is disabled $wallet->setSetting('mandate_disabled', 1); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); // Expect no payment if balance is ok $wallet->setSetting('mandate_disabled', null); $wallet->balance = 1000; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); // Expect no payment if the top-up amount is not enough $wallet->setSetting('mandate_disabled', null); $wallet->balance = -2050; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) { $job_wallet = $this->getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); // Expect no payment if there's no mandate $wallet->setSetting('mollie_mandate_id', null); $wallet->balance = 0; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); $this->unmockStripe(); // Test webhook $post = [ 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", 'object' => "event", 'api_version' => "2020-03-02", 'created' => 1590147209, 'data' => [ 'object' => [ 'id' => $payment->id, 'object' => "payment_intent", 'amount' => 2010, 'capture_method' => "automatic", 'created' => 1590147204, 'currency' => "chf", 'customer' => "cus_HKDZ53OsKdlM83", 'last_payment_error' => null, 'metadata' => [], 'receipt_email' => "payment-test@kolabnow.com", 'status' => "succeeded" ] ], 'type' => "payment_intent.succeeded" ]; // Test payment succeeded event $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(2010, $wallet->fresh()->balance); - $transaction = $wallet->transactions()->where('type', Transaction::WALLET_CREDIT)->last(); + $transaction = $wallet->transactions() + ->where('type', Transaction::WALLET_CREDIT)->get()->last(); + $this->assertSame(2010, $transaction->amount); $this->assertSame( "Auto-payment transaction {$payment->id} using Stripe", $transaction->description ); // Assert that email notification job has been dispatched Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); Bus::fake(); // Test for payment failure ('failed' status) $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $wallet->setSetting('mandate_disabled', null); $post['type'] = "payment_intent.payment_failed"; $post['data']['object']['status'] = 'failed'; $response = $this->webhookRequest($post); $response->assertStatus(200); $wallet->refresh(); $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); $this->assertEquals(2010, $wallet->balance); $this->assertTrue(!empty($wallet->getSetting('mandate_disabled'))); // Assert that email notification job has been dispatched Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); Bus::fake(); // Test for payment failure ('canceled' status) $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $post['type'] = "payment_intent.canceled"; $post['data']['object']['status'] = 'canceled'; $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_CANCELED, $payment->fresh()->status); $this->assertEquals(2010, $wallet->fresh()->balance); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); } /** * Generate Stripe-Signature header for a webhook payload */ protected function webhookRequest($post) { $secret = \config('services.stripe.webhook_secret'); $ts = time(); $payload = "$ts." . json_encode($post); $sig = sprintf('t=%d,v1=%s', $ts, \hash_hmac('sha256', $payload, $secret)); return $this->withHeaders(['Stripe-Signature' => $sig]) ->json('POST', "api/webhooks/payment/stripe", $post); } } diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php index 81ffc966..dc4769c0 100644 --- a/src/tests/Feature/Controller/UsersTest.php +++ b/src/tests/Feature/Controller/UsersTest.php @@ -1,1005 +1,1007 @@ deleteTestUser('jane@kolabnow.com'); $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); $this->deleteTestUser('UsersControllerTest2@userscontroller.com'); $this->deleteTestUser('UsersControllerTest3@userscontroller.com'); $this->deleteTestUser('UserEntitlement2A@UserEntitlement.com'); $this->deleteTestUser('john2.doe2@kolab.org'); $this->deleteTestDomain('userscontroller.com'); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->discount()->dissociate(); $wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete(); $wallet->save(); $user->status |= User::STATUS_IMAP_READY; $user->save(); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestUser('jane@kolabnow.com'); $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); $this->deleteTestUser('UsersControllerTest2@userscontroller.com'); $this->deleteTestUser('UsersControllerTest3@userscontroller.com'); $this->deleteTestUser('UserEntitlement2A@UserEntitlement.com'); $this->deleteTestUser('john2.doe2@kolab.org'); $this->deleteTestDomain('userscontroller.com'); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->discount()->dissociate(); $wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete(); $wallet->save(); $user->status |= User::STATUS_IMAP_READY; $user->save(); parent::tearDown(); } /** * Test user deleting (DELETE /api/v4/users/) */ public function testDestroy(): void { // First create some users/accounts to delete $package_kolab = \App\Package::where('title', 'kolab')->first(); $package_domain = \App\Package::where('title', 'domain-hosting')->first(); $john = $this->getTestUser('john@kolab.org'); $user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com'); $user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $user1->assignPackage($package_kolab); $domain->assignPackage($package_domain, $user1); $user1->assignPackage($package_kolab, $user2); $user1->assignPackage($package_kolab, $user3); // Test unauth access $response = $this->delete("api/v4/users/{$user2->id}"); $response->assertStatus(401); // Test access to other user/account $response = $this->actingAs($john)->delete("api/v4/users/{$user2->id}"); $response->assertStatus(403); $response = $this->actingAs($john)->delete("api/v4/users/{$user1->id}"); $response->assertStatus(403); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test that non-controller cannot remove himself $response = $this->actingAs($user3)->delete("api/v4/users/{$user3->id}"); $response->assertStatus(403); // Test removing a non-controller user $response = $this->actingAs($user1)->delete("api/v4/users/{$user3->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('User deleted successfully.', $json['message']); // Test removing self (an account with users) $response = $this->actingAs($user1)->delete("api/v4/users/{$user1->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('User deleted successfully.', $json['message']); } /** * Test user deleting (DELETE /api/v4/users/) */ public function testDestroyByController(): void { // Create an account with additional controller - $user2 $package_kolab = \App\Package::where('title', 'kolab')->first(); $package_domain = \App\Package::where('title', 'domain-hosting')->first(); $user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com'); $user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $user1->assignPackage($package_kolab); $domain->assignPackage($package_domain, $user1); $user1->assignPackage($package_kolab, $user2); $user1->assignPackage($package_kolab, $user3); $user1->wallets()->first()->addController($user2); // TODO/FIXME: // For now controller can delete himself, as well as // the whole account he has control to, including the owner // Probably he should not be able to do none of those // However, this is not 0-regression scenario as we // do not fully support additional controllers. //$response = $this->actingAs($user2)->delete("api/v4/users/{$user2->id}"); //$response->assertStatus(403); $response = $this->actingAs($user2)->delete("api/v4/users/{$user3->id}"); $response->assertStatus(200); $response = $this->actingAs($user2)->delete("api/v4/users/{$user1->id}"); $response->assertStatus(200); // Note: More detailed assertions in testDestroy() above $this->assertTrue($user1->fresh()->trashed()); $this->assertTrue($user2->fresh()->trashed()); $this->assertTrue($user3->fresh()->trashed()); } /** * Test user listing (GET /api/v4/users) */ public function testIndex(): void { // Test unauth access $response = $this->get("api/v4/users"); $response->assertStatus(401); $jack = $this->getTestUser('jack@kolab.org'); $joe = $this->getTestUser('joe@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $response = $this->actingAs($jack)->get("/api/v4/users"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(0, $json); $response = $this->actingAs($john)->get("/api/v4/users"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame($jack->email, $json[0]['email']); $this->assertSame($joe->email, $json[1]['email']); $this->assertSame($john->email, $json[2]['email']); $this->assertSame($ned->email, $json[3]['email']); // Values below are tested by Unit tests $this->assertArrayHasKey('isDeleted', $json[0]); $this->assertArrayHasKey('isSuspended', $json[0]); $this->assertArrayHasKey('isActive', $json[0]); $this->assertArrayHasKey('isLdapReady', $json[0]); $this->assertArrayHasKey('isImapReady', $json[0]); $response = $this->actingAs($ned)->get("/api/v4/users"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame($jack->email, $json[0]['email']); $this->assertSame($joe->email, $json[1]['email']); $this->assertSame($john->email, $json[2]['email']); $this->assertSame($ned->email, $json[3]['email']); } /** * Test fetching user data/profile (GET /api/v4/users/) */ public function testShow(): void { $userA = $this->getTestUser('UserEntitlement2A@UserEntitlement.com'); // Test getting profile of self $response = $this->actingAs($userA)->get("/api/v4/users/{$userA->id}"); $json = $response->json(); $response->assertStatus(200); $this->assertEquals($userA->id, $json['id']); $this->assertEquals($userA->email, $json['email']); $this->assertTrue(is_array($json['statusInfo'])); $this->assertTrue(is_array($json['settings'])); $this->assertTrue(is_array($json['aliases'])); $this->assertSame([], $json['skus']); // Values below are tested by Unit tests $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isSuspended', $json); $this->assertArrayHasKey('isActive', $json); $this->assertArrayHasKey('isLdapReady', $json); $this->assertArrayHasKey('isImapReady', $json); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); // Test unauthorized access to a profile of other user $response = $this->actingAs($jack)->get("/api/v4/users/{$userA->id}"); $response->assertStatus(403); // Test authorized access to a profile of other user // Ned: Additional account controller $response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}"); $response->assertStatus(200); $response = $this->actingAs($ned)->get("/api/v4/users/{$jack->id}"); $response->assertStatus(200); // John: Account owner $response = $this->actingAs($john)->get("/api/v4/users/{$jack->id}"); $response->assertStatus(200); $response = $this->actingAs($john)->get("/api/v4/users/{$ned->id}"); $response->assertStatus(200); $json = $response->json(); $storage_sku = Sku::where('title', 'storage')->first(); $groupware_sku = Sku::where('title', 'groupware')->first(); $mailbox_sku = Sku::where('title', 'mailbox')->first(); $secondfactor_sku = Sku::where('title', '2fa')->first(); $this->assertCount(5, $json['skus']); $this->assertSame(2, $json['skus'][$storage_sku->id]['count']); $this->assertSame(1, $json['skus'][$groupware_sku->id]['count']); $this->assertSame(1, $json['skus'][$mailbox_sku->id]['count']); $this->assertSame(1, $json['skus'][$secondfactor_sku->id]['count']); } /** * Test fetching user status (GET /api/v4/users//status) * and forcing setup process update (?refresh=1) * * @group imap * @group dns */ public function testStatus(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/users/{$john->id}/status"); $response->assertStatus(403); - $john->status ^= User::STATUS_IMAP_READY; - $john->save(); + if ($john->isImapReady()) { + $john->status ^= User::STATUS_IMAP_READY; + $john->save(); + } // Get user status $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse($json['isImapReady']); $this->assertFalse($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('user-imap-ready', $json['process'][2]['label']); $this->assertSame(false, $json['process'][2]['state']); $this->assertTrue(empty($json['status'])); $this->assertTrue(empty($json['message'])); // Make sure the domain is confirmed (other test might unset that status) $domain = $this->getTestDomain('kolab.org'); $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); // Now "reboot" the process and verify the user in imap synchronously $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isImapReady']); $this->assertTrue($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('user-imap-ready', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); } /** * Test UsersController::statusInfo() */ public function testStatusInfo(): void { $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $user->created_at = Carbon::now(); $user->status = User::STATUS_NEW; $user->save(); $result = UsersController::statusInfo($user); $this->assertFalse($result['isReady']); $this->assertCount(3, $result['process']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('user-ldap-ready', $result['process'][1]['label']); $this->assertSame(false, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(false, $result['process'][2]['state']); $this->assertSame('running', $result['processState']); $user->created_at = Carbon::now()->subSeconds(181); $user->save(); $result = UsersController::statusInfo($user); $this->assertSame('failed', $result['processState']); $user->status |= User::STATUS_LDAP_READY | User::STATUS_IMAP_READY; $user->save(); $result = UsersController::statusInfo($user); $this->assertTrue($result['isReady']); $this->assertCount(3, $result['process']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('user-ldap-ready', $result['process'][1]['label']); $this->assertSame(true, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(true, $result['process'][2]['state']); $this->assertSame('done', $result['processState']); $domain->status |= Domain::STATUS_VERIFIED; $domain->type = Domain::TYPE_EXTERNAL; $domain->save(); $result = UsersController::statusInfo($user); $this->assertFalse($result['isReady']); $this->assertCount(7, $result['process']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('user-ldap-ready', $result['process'][1]['label']); $this->assertSame(true, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(true, $result['process'][2]['state']); $this->assertSame('domain-new', $result['process'][3]['label']); $this->assertSame(true, $result['process'][3]['state']); $this->assertSame('domain-ldap-ready', $result['process'][4]['label']); $this->assertSame(false, $result['process'][4]['state']); $this->assertSame('domain-verified', $result['process'][5]['label']); $this->assertSame(true, $result['process'][5]['state']); $this->assertSame('domain-confirmed', $result['process'][6]['label']); $this->assertSame(false, $result['process'][6]['state']); } /** * Test user creation (POST /api/v4/users) */ public function testStore(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test empty request $response = $this->actingAs($john)->post("/api/v4/users", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The email field is required.", $json['errors']['email']); $this->assertSame("The password field is required.", $json['errors']['password'][0]); $this->assertCount(2, $json); // Test access by user not being a wallet controller $post = ['first_name' => 'Test']; $response = $this->actingAs($jack)->post("/api/v4/users", $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 = ['password' => '12345678', 'email' => 'invalid']; $response = $this->actingAs($john)->post("/api/v4/users", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The password confirmation does not match.', $json['errors']['password'][0]); $this->assertSame('The specified email is invalid.', $json['errors']['email']); // Test existing user email $post = [ 'password' => 'simple', 'password_confirmation' => 'simple', 'first_name' => 'John2', 'last_name' => 'Doe2', 'email' => 'jack.daniels@kolab.org', ]; $response = $this->actingAs($john)->post("/api/v4/users", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified email is not available.', $json['errors']['email']); $package_kolab = \App\Package::where('title', 'kolab')->first(); $package_domain = \App\Package::where('title', 'domain-hosting')->first(); $post = [ 'password' => 'simple', 'password_confirmation' => 'simple', 'first_name' => 'John2', 'last_name' => 'Doe2', 'email' => 'john2.doe2@kolab.org', 'organization' => 'TestOrg', 'aliases' => ['useralias1@kolab.org', 'useralias2@kolab.org'], ]; // Missing package $response = $this->actingAs($john)->post("/api/v4/users", $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_domain->id; $response = $this->actingAs($john)->post("/api/v4/users", $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_kolab->id; $response = $this->actingAs($john)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("User created successfully.", $json['message']); $this->assertCount(2, $json); $user = User::where('email', 'john2.doe2@kolab.org')->first(); $this->assertInstanceOf(User::class, $user); $this->assertSame('John2', $user->getSetting('first_name')); $this->assertSame('Doe2', $user->getSetting('last_name')); $this->assertSame('TestOrg', $user->getSetting('organization')); $aliases = $user->aliases()->orderBy('alias')->get(); $this->assertCount(2, $aliases); $this->assertSame('useralias1@kolab.org', $aliases[0]->alias); $this->assertSame('useralias2@kolab.org', $aliases[1]->alias); // Assert the new user entitlements $this->assertUserEntitlements($user, ['groupware', 'mailbox', 'storage', 'storage']); // Assert the wallet to which the new user should be assigned to $wallet = $user->wallet(); $this->assertSame($john->wallets()->first()->id, $wallet->id); // Test acting as account controller (not owner) /* // FIXME: How do we know to which wallet the new user should be assigned to? $this->deleteTestUser('john2.doe2@kolab.org'); $response = $this->actingAs($ned)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); */ $this->markTestIncomplete(); } /** * Test user update (PUT /api/v4/users/) */ public function testUpdate(): void { $userA = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $domain = $this->getTestDomain( 'userscontroller.com', ['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL] ); // Test unauthorized update of other user profile $response = $this->actingAs($jack)->get("/api/v4/users/{$userA->id}", []); $response->assertStatus(403); // Test authorized update of account owner by account controller $response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}", []); $response->assertStatus(200); // Test updating of self (empty request) $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", []); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame("User data updated successfully.", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['password' => '12345678', 'currency' => 'invalid']; $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The password confirmation does not match.', $json['errors']['password'][0]); $this->assertSame('The currency must be 3 characters.', $json['errors']['currency'][0]); // Test full profile update including password $post = [ 'password' => 'simple', 'password_confirmation' => 'simple', 'first_name' => 'John2', 'last_name' => 'Doe2', 'organization' => 'TestOrg', 'phone' => '+123 123 123', 'external_email' => 'external@gmail.com', 'billing_address' => 'billing', 'country' => 'CH', 'currency' => 'CHF', 'aliases' => ['useralias1@' . \config('app.domain'), 'useralias2@' . \config('app.domain')] ]; $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("User data updated successfully.", $json['message']); $this->assertCount(2, $json); $this->assertTrue($userA->password != $userA->fresh()->password); unset($post['password'], $post['password_confirmation'], $post['aliases']); foreach ($post as $key => $value) { $this->assertSame($value, $userA->getSetting($key)); } $aliases = $userA->aliases()->orderBy('alias')->get(); $this->assertCount(2, $aliases); $this->assertSame('useralias1@' . \config('app.domain'), $aliases[0]->alias); $this->assertSame('useralias2@' . \config('app.domain'), $aliases[1]->alias); // Test unsetting values $post = [ 'first_name' => '', 'last_name' => '', 'organization' => '', 'phone' => '', 'external_email' => '', 'billing_address' => '', 'country' => '', 'currency' => '', 'aliases' => ['useralias2@' . \config('app.domain')] ]; $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("User data updated successfully.", $json['message']); $this->assertCount(2, $json); unset($post['aliases']); foreach ($post as $key => $value) { $this->assertNull($userA->getSetting($key)); } $aliases = $userA->aliases()->get(); $this->assertCount(1, $aliases); $this->assertSame('useralias2@' . \config('app.domain'), $aliases[0]->alias); // Test error on setting an alias to other user's domain // and missing password confirmation $post = [ 'password' => 'simple123', 'aliases' => ['useralias2@' . \config('app.domain'), 'useralias1@kolab.org'] ]; $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertCount(1, $json['errors']['aliases']); $this->assertSame("The specified domain is not available.", $json['errors']['aliases'][1]); $this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]); // Test authorized update of other user $response = $this->actingAs($ned)->get("/api/v4/users/{$jack->id}", []); $response->assertStatus(200); // TODO: Test error on aliases with invalid/non-existing/other-user's domain // Create entitlements and additional user for following tests $owner = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $user = $this->getTestUser('UsersControllerTest2@userscontroller.com'); $package_domain = Package::where('title', 'domain-hosting')->first(); $package_kolab = Package::where('title', 'kolab')->first(); $package_lite = Package::where('title', 'lite')->first(); $sku_mailbox = Sku::where('title', 'mailbox')->first(); $sku_storage = Sku::where('title', 'storage')->first(); $sku_groupware = Sku::where('title', 'groupware')->first(); $domain = $this->getTestDomain( 'userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ] ); $domain->assignPackage($package_domain, $owner); $owner->assignPackage($package_kolab); $owner->assignPackage($package_lite, $user); // Non-controller cannot update his own entitlements $post = ['skus' => []]; $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post); $response->assertStatus(422); // Test updating entitlements $post = [ 'skus' => [ $sku_mailbox->id => 1, $sku_storage->id => 3, $sku_groupware->id => 1, ], ]; $response = $this->actingAs($owner)->put("/api/v4/users/{$user->id}", $post); $response->assertStatus(200); $storage_cost = $user->entitlements() ->where('sku_id', $sku_storage->id) ->orderBy('cost') ->pluck('cost')->all(); $this->assertUserEntitlements( $user, ['groupware', 'mailbox', 'storage', 'storage', 'storage'] ); $this->assertSame([0, 0, 25], $storage_cost); } /** * Test UsersController::updateEntitlements() */ public function testUpdateEntitlements(): void { $jane = $this->getTestUser('jane@kolabnow.com'); $kolab = \App\Package::where('title', 'kolab')->first(); $storage = \App\Sku::where('title', 'storage')->first(); $activesync = \App\Sku::where('title', 'activesync')->first(); $groupware = \App\Sku::where('title', 'groupware')->first(); $mailbox = \App\Sku::where('title', 'mailbox')->first(); // standard package, 1 mailbox, 1 groupware, 2 storage $jane->assignPackage($kolab); // add 2 storage, 1 activesync $post = [ 'skus' => [ $mailbox->id => 1, $groupware->id => 1, $storage->id => 4, $activesync->id => 1 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(200); $this->assertUserEntitlements( $jane, [ 'activesync', 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage' ] ); // add 2 storage, remove 1 activesync $post = [ 'skus' => [ $mailbox->id => 1, $groupware->id => 1, $storage->id => 6, $activesync->id => 0 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(200); $this->assertUserEntitlements( $jane, [ 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage' ] ); // add mailbox $post = [ 'skus' => [ $mailbox->id => 2, $groupware->id => 1, $storage->id => 6, $activesync->id => 0 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(500); $this->assertUserEntitlements( $jane, [ 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage' ] ); // remove mailbox $post = [ 'skus' => [ $mailbox->id => 0, $groupware->id => 1, $storage->id => 6, $activesync->id => 0 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(500); $this->assertUserEntitlements( $jane, [ 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage' ] ); // less than free storage $post = [ 'skus' => [ $mailbox->id => 1, $groupware->id => 1, $storage->id => 1, $activesync->id => 0 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(200); $this->assertUserEntitlements( $jane, [ 'groupware', 'mailbox', 'storage', 'storage' ] ); } /** * Test user data response used in show and info actions */ public function testUserResponse(): void { $provider = \config('services.payment_provider') ?: 'mollie'; $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->setSettings(['mollie_id' => null, 'stripe_id' => null]); $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]); $this->assertEquals($user->id, $result['id']); $this->assertEquals($user->email, $result['email']); $this->assertEquals($user->status, $result['status']); $this->assertTrue(is_array($result['statusInfo'])); $this->assertTrue(is_array($result['aliases'])); $this->assertCount(1, $result['aliases']); $this->assertSame('john.doe@kolab.org', $result['aliases'][0]); $this->assertTrue(is_array($result['settings'])); $this->assertSame('US', $result['settings']['country']); $this->assertSame('USD', $result['settings']['currency']); $this->assertTrue(is_array($result['accounts'])); $this->assertTrue(is_array($result['wallets'])); $this->assertCount(0, $result['accounts']); $this->assertCount(1, $result['wallets']); $this->assertSame($wallet->id, $result['wallet']['id']); $this->assertArrayNotHasKey('discount', $result['wallet']); $ned = $this->getTestUser('ned@kolab.org'); $ned_wallet = $ned->wallets()->first(); $result = $this->invokeMethod(new UsersController(), 'userResponse', [$ned]); $this->assertEquals($ned->id, $result['id']); $this->assertEquals($ned->email, $result['email']); $this->assertTrue(is_array($result['accounts'])); $this->assertTrue(is_array($result['wallets'])); $this->assertCount(1, $result['accounts']); $this->assertCount(1, $result['wallets']); $this->assertSame($wallet->id, $result['wallet']['id']); $this->assertSame($wallet->id, $result['accounts'][0]['id']); $this->assertSame($ned_wallet->id, $result['wallets'][0]['id']); $this->assertSame($provider, $result['wallet']['provider']); $this->assertSame($provider, $result['wallets'][0]['provider']); // Test discount in a response $discount = Discount::where('code', 'TEST')->first(); $wallet->discount()->associate($discount); $wallet->save(); $mod_provider = $provider == 'mollie' ? 'stripe' : 'mollie'; $wallet->setSetting($mod_provider . '_id', 123); $user->refresh(); $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]); $this->assertEquals($user->id, $result['id']); $this->assertSame($discount->id, $result['wallet']['discount_id']); $this->assertSame($discount->discount, $result['wallet']['discount']); $this->assertSame($discount->description, $result['wallet']['discount_description']); $this->assertSame($mod_provider, $result['wallet']['provider']); $this->assertSame($discount->id, $result['wallets'][0]['discount_id']); $this->assertSame($discount->discount, $result['wallets'][0]['discount']); $this->assertSame($discount->description, $result['wallets'][0]['discount_description']); $this->assertSame($mod_provider, $result['wallets'][0]['provider']); } /** * List of alias validation cases for testValidateEmail() * * @return array Arguments for testValidateEmail() */ public function dataValidateEmail(): array { $this->refreshApplication(); $public_domains = Domain::getPublicDomains(); $domain = reset($public_domains); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); return [ // Invalid format ["$domain", $john, true, 'The specified alias is invalid.'], [".@$domain", $john, true, 'The specified alias is invalid.'], ["test123456@localhost", $john, true, 'The specified domain is invalid.'], ["test123456@unknown-domain.org", $john, true, 'The specified domain is invalid.'], ["$domain", $john, false, 'The specified email is invalid.'], [".@$domain", $john, false, 'The specified email is invalid.'], // forbidden local part on public domains ["admin@$domain", $john, true, 'The specified alias is not available.'], ["administrator@$domain", $john, true, 'The specified alias is not available.'], // forbidden (other user's domain) ["testtest@kolab.org", $user, true, 'The specified domain is not available.'], // existing alias of other user, to be a user email ["jack.daniels@kolab.org", $john, false, 'The specified email is not available.'], // existing alias of other user, to be an alias, user in the same group account ["jack.daniels@kolab.org", $john, true, null], // existing user ["jack@kolab.org", $john, true, 'The specified alias is not available.'], // valid (user domain) ["admin@kolab.org", $john, true, null], // valid (public domain) ["test.test@$domain", $john, true, null], ]; } /** * User email/alias validation. * * Note: Technically these include unit tests, but let's keep it here for now. * FIXME: Shall we do a http request for each case? * * @dataProvider dataValidateEmail */ public function testValidateEmail($alias, $user, $is_alias, $expected_result): void { $args = [$alias, $user, $is_alias]; $result = $this->invokeMethod(new UsersController(), 'validateEmail', $args); $this->assertSame($expected_result, $result); } } diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php index b8ee06f5..8e0a5437 100644 --- a/src/tests/Feature/Controller/WalletsTest.php +++ b/src/tests/Feature/Controller/WalletsTest.php @@ -1,333 +1,333 @@ deleteTestUser('wallets-controller@kolabnow.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestUser('wallets-controller@kolabnow.com'); parent::tearDown(); } /** * Test for getWalletNotice() method */ public function testGetWalletNotice(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); $package = \App\Package::where('title', 'kolab')->first(); $user->assignPackage($package); $wallet = $user->wallets()->first(); $controller = new WalletsController(); $method = new \ReflectionMethod($controller, 'getWalletNotice'); $method->setAccessible(true); // User/entitlements created today, balance=0 $notice = $method->invoke($controller, $wallet); $this->assertSame('You are in your free trial period.', $notice); $wallet->owner->created_at = Carbon::now()->subDays(15); $wallet->owner->save(); $notice = $method->invoke($controller, $wallet); $this->assertSame('Your free trial is about to end, top up to continue.', $notice); // User/entitlements created today, balance=-10 CHF $wallet->balance = -1000; $notice = $method->invoke($controller, $wallet); $this->assertSame('You are out of credit, top up your balance now.', $notice); // User/entitlements created today, balance=-9,99 CHF (monthly cost) $wallet->balance = 999; $notice = $method->invoke($controller, $wallet); - $this->assertTrue(strpos($notice, '(1 month)') !== false); + $this->assertRegExp('/\((1 month|4 weeks)\)/', $notice); // Old entitlements, 100% discount $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subDays(40)); $discount = \App\Discount::where('discount', 100)->first(); $wallet->discount()->associate($discount); $notice = $method->invoke($controller, $wallet->refresh()); $this->assertSame(null, $notice); } /** * Test fetching pdf receipt */ public function testReceiptDownload(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); $john = $this->getTestUser('john@klab.org'); $wallet = $user->wallets()->first(); // Unauth access not allowed $response = $this->get("api/v4/wallets/{$wallet->id}/receipts/2020-05"); $response->assertStatus(401); $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/receipts/2020-05"); $response->assertStatus(403); // Invalid receipt id (current month) $receiptId = date('Y-m'); $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); $response->assertStatus(404); // Invalid receipt id $receiptId = '1000-03'; $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); $response->assertStatus(404); // Valid receipt id $year = intval(date('Y')) - 1; $receiptId = "$year-12"; $filename = \config('app.name') . " Receipt for $year-12"; $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); $response->assertStatus(200); $response->assertHeader('content-type', 'application/pdf'); $response->assertHeader('content-disposition', 'attachment; filename="' . $filename . '"'); $response->assertHeader('content-length'); $length = $response->headers->get('content-length'); $content = $response->content(); $this->assertStringStartsWith("%PDF-1.3\n", $content); $this->assertEquals(strlen($content), $length); } /** * Test fetching list of receipts */ public function testReceipts(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); $john = $this->getTestUser('john@klab.org'); $wallet = $user->wallets()->first(); $wallet->payments()->delete(); // Unauth access not allowed $response = $this->get("api/v4/wallets/{$wallet->id}/receipts"); $response->assertStatus(401); $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/receipts"); $response->assertStatus(403); // Empty list expected $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame([], $json['list']); $this->assertSame(1, $json['page']); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); // Insert a payment to the database $date = Carbon::create(intval(date('Y')) - 1, 4, 30); $payment = Payment::create([ 'id' => 'AAA1', 'status' => PaymentProvider::STATUS_PAID, 'type' => PaymentProvider::TYPE_ONEOFF, 'description' => 'Paid in April', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 1111, ]); $payment->updated_at = $date; $payment->save(); $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame([$date->format('Y-m')], $json['list']); $this->assertSame(1, $json['page']); $this->assertSame(1, $json['count']); $this->assertSame(false, $json['hasMore']); } /** * Test fetching a wallet (GET /api/v4/wallets/:id) */ public function testShow(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $wallet = $john->wallets()->first(); // Accessing a wallet of someone else $response = $this->actingAs($jack)->get("api/v4/wallets/{$wallet->id}"); $response->assertStatus(403); // Accessing non-existing wallet $response = $this->actingAs($jack)->get("api/v4/wallets/aaa"); $response->assertStatus(404); // Wallet owner $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($wallet->id, $json['id']); $this->assertSame('CHF', $json['currency']); $this->assertSame($wallet->balance, $json['balance']); $this->assertTrue(empty($json['description'])); $this->assertTrue(!empty($json['notice'])); } /** * Test fetching wallet transactions */ public function testTransactions(): void { $package_kolab = \App\Package::where('title', 'kolab')->first(); $user = $this->getTestUser('wallets-controller@kolabnow.com'); $user->assignPackage($package_kolab); $john = $this->getTestUser('john@klab.org'); $wallet = $user->wallets()->first(); // Unauth access not allowed $response = $this->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(401); $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(403); // Expect empty list $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame([], $json['list']); $this->assertSame(1, $json['page']); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); // Create some sample transactions $transactions = $this->createTestTransactions($wallet); $transactions = array_reverse($transactions); $pages = array_chunk($transactions, 10 /* page size*/); // Get the first page $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(1, $json['page']); $this->assertSame(10, $json['count']); $this->assertSame(true, $json['hasMore']); $this->assertCount(10, $json['list']); foreach ($pages[0] as $idx => $transaction) { $this->assertSame($transaction->id, $json['list'][$idx]['id']); $this->assertSame($transaction->type, $json['list'][$idx]['type']); $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); $this->assertFalse($json['list'][$idx]['hasDetails']); $this->assertFalse(array_key_exists('user', $json['list'][$idx])); } $search = null; // Get the second page $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?page=2"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(2, $json['page']); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertCount(2, $json['list']); foreach ($pages[1] as $idx => $transaction) { $this->assertSame($transaction->id, $json['list'][$idx]['id']); $this->assertSame($transaction->type, $json['list'][$idx]['type']); $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); $this->assertSame( $transaction->type == Transaction::WALLET_DEBIT, $json['list'][$idx]['hasDetails'] ); $this->assertFalse(array_key_exists('user', $json['list'][$idx])); if ($transaction->type == Transaction::WALLET_DEBIT) { $search = $transaction->id; } } // Get a non-existing page $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?page=3"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(3, $json['page']); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertCount(0, $json['list']); // Sub-transaction searching $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?transaction=123"); $response->assertStatus(404); $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?transaction={$search}"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(1, $json['page']); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertCount(2, $json['list']); $this->assertSame(Transaction::ENTITLEMENT_BILLED, $json['list'][0]['type']); $this->assertSame(Transaction::ENTITLEMENT_BILLED, $json['list'][1]['type']); // Test that John gets 404 if he tries to access // someone else's transaction ID on his wallet's endpoint $wallet = $john->wallets()->first(); $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/transactions?transaction={$search}"); $response->assertStatus(404); } } diff --git a/src/tests/Feature/Jobs/UserVerifyTest.php b/src/tests/Feature/Jobs/UserVerifyTest.php index ba68689b..2876bc8b 100644 --- a/src/tests/Feature/Jobs/UserVerifyTest.php +++ b/src/tests/Feature/Jobs/UserVerifyTest.php @@ -1,60 +1,62 @@ getTestUser('ned@kolab.org'); $ned->status |= User::STATUS_IMAP_READY; $ned->save(); } public function tearDown(): void { $ned = $this->getTestUser('ned@kolab.org'); $ned->status |= User::STATUS_IMAP_READY; $ned->save(); parent::tearDown(); } /** * Test job handle * * @group imap */ public function testHandle(): void { Queue::fake(); $user = $this->getTestUser('ned@kolab.org'); - $user->status ^= User::STATUS_IMAP_READY; - $user->save(); + if ($user->isImapReady()) { + $user->status ^= User::STATUS_IMAP_READY; + $user->save(); + } $this->assertFalse($user->isImapReady()); for ($i = 0; $i < 10; $i++) { $job = new UserVerify($user); $job->handle(); if ($user->fresh()->isImapReady()) { $this->assertTrue(true); return; } sleep(1); } $this->assertTrue(false, "Unable to verify the IMAP account is set up in time"); } } diff --git a/src/tests/Unit/DomainTest.php b/src/tests/Unit/DomainTest.php index 95b5d7d1..46b7b4ba 100644 --- a/src/tests/Unit/DomainTest.php +++ b/src/tests/Unit/DomainTest.php @@ -1,117 +1,117 @@ 'test.com', 'status' => \array_sum($domain_statuses), 'type' => Domain::TYPE_EXTERNAL ] ); $this->assertTrue($domain->isNew() === in_array(Domain::STATUS_NEW, $domain_statuses)); $this->assertTrue($domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domain_statuses)); $this->assertTrue($domain->isConfirmed() === in_array(Domain::STATUS_CONFIRMED, $domain_statuses)); $this->assertTrue($domain->isSuspended() === in_array(Domain::STATUS_SUSPENDED, $domain_statuses)); $this->assertTrue($domain->isDeleted() === in_array(Domain::STATUS_DELETED, $domain_statuses)); $this->assertTrue($domain->isLdapReady() === in_array(Domain::STATUS_LDAP_READY, $domain_statuses)); -// $this->assertTrue($domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domain_statuses)); + $this->assertTrue($domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domain_statuses)); } } /** * Test setStatusAttribute exception */ public function testDomainStatusInvalid(): void { $this->expectException(\Exception::class); $domain = new Domain( [ 'namespace' => 'test.com', 'status' => 1234567, ] ); } /** * Test basic Domain funtionality */ public function testDomainType(): void { $types = [ Domain::TYPE_PUBLIC, Domain::TYPE_HOSTED, Domain::TYPE_EXTERNAL, ]; $domains = \App\Utils::powerSet($types); foreach ($domains as $domain_types) { $domain = new Domain( [ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, 'type' => \array_sum($domain_types), ] ); $this->assertTrue($domain->isPublic() === in_array(Domain::TYPE_PUBLIC, $domain_types)); $this->assertTrue($domain->isHosted() === in_array(Domain::TYPE_HOSTED, $domain_types)); $this->assertTrue($domain->isExternal() === in_array(Domain::TYPE_EXTERNAL, $domain_types)); } } /** * Test domain hash generation */ public function testHash(): void { $domain = new Domain([ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, ]); $hash_code = $domain->hash(); $this->assertRegExp('/^[a-f0-9]{32}$/', $hash_code); $hash_text = $domain->hash(Domain::HASH_TEXT); $this->assertRegExp('/^kolab-verify=[a-f0-9]{32}$/', $hash_text); $this->assertSame($hash_code, str_replace('kolab-verify=', '', $hash_text)); $hash_cname = $domain->hash(Domain::HASH_CNAME); $this->assertSame('kolab-verify', $hash_cname); $hash_code2 = $domain->hash(Domain::HASH_CODE); $this->assertSame($hash_code, $hash_code2); } }