Page MenuHomePhorge

No OneTemporary

diff --git a/src/app/Http/Middleware/DevelConfig.php b/src/app/Http/Middleware/DevelConfig.php
index 83c53874..2ac29385 100644
--- a/src/app/Http/Middleware/DevelConfig.php
+++ b/src/app/Http/Middleware/DevelConfig.php
@@ -1,52 +1,38 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Cache;
class DevelConfig
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
// Only in testing/local environment...
if (\App::environment('local')) {
- // This is the only way I found to change configuration
- // on a running application. We need this to browser-test both
- // Mollie and Stripe providers without .env file modification
- // and artisan restart
- if ($request->getMethod() == 'GET' && isset($request->paymentProvider)) {
- $provider = $request->paymentProvider;
- } else {
- $provider = $request->headers->get('X-TEST-PAYMENT-PROVIDER');
- }
-
- if (!empty($provider)) {
- \config(['services.payment_provider' => $provider]);
- }
-
// Pick up config set in Tests\Browser::withConfig
// This wouldn't technically need to be in a middleware,
// but this way we ensure it's propagated during the next request.
if (Cache::has('duskconfig')) {
$configJson = Cache::get('duskconfig');
$configValues = json_decode($configJson, true);
if (!empty($configValues)) {
foreach ($configValues as $key => $value) {
\config([$key => $value]);
}
}
}
}
return $next($request);
}
}
diff --git a/src/app/Observers/SignupCodeObserver.php b/src/app/Observers/SignupCodeObserver.php
index 23c3900b..5e0cbee1 100644
--- a/src/app/Observers/SignupCodeObserver.php
+++ b/src/app/Observers/SignupCodeObserver.php
@@ -1,77 +1,77 @@
<?php
namespace App\Observers;
use App\SignupCode;
use Carbon\Carbon;
use Illuminate\Support\Str;
class SignupCodeObserver
{
/**
* Handle the "creating" event.
*
* Ensure that the code entry is created with a random code/short_code.
*
* @param \App\SignupCode $code The code being created.
*
* @return void
*/
public function creating(SignupCode $code): void
{
$code_length = SignupCode::CODE_LENGTH;
$exp_hours = env('SIGNUP_CODE_EXPIRY', SignupCode::CODE_EXP_HOURS);
if (empty($code->code)) {
$code->short_code = SignupCode::generateShortCode();
// FIXME: Replace this with something race-condition free
while (true) {
$code->code = Str::random($code_length);
if (!SignupCode::find($code->code)) {
break;
}
}
}
$code->headers = collect(request()->headers->all())
->filter(function ($value, $key) {
// remove some headers we don't care about
- return !in_array($key, ['cookie', 'referer', 'x-test-payment-provider', 'origin']);
+ return !in_array($key, ['cookie', 'referer', 'origin']);
})
->map(function ($value) {
return is_array($value) && count($value) == 1 ? $value[0] : $value;
})
->all();
$code->expires_at = Carbon::now()->addHours($exp_hours);
$code->ip_address = request()->ip();
if ($code->email && strpos($code->email, '@')) {
$parts = explode('@', $code->email);
$code->local_part = $parts[0];
$code->domain_part = $parts[1];
}
}
/**
* Handle the "updating" event.
*
* @param SignupCode $code The code being updated.
*
* @return void
*/
public function updating(SignupCode $code)
{
if ($code->email && strpos($code->email, '@')) {
$parts = explode('@', $code->email);
$code->local_part = $parts[0];
$code->domain_part = $parts[1];
} else {
$code->local_part = null;
$code->domain_part = null;
}
}
}
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
index 1fc32163..77047f60 100644
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -1,505 +1,501 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap')
import AppComponent from '../vue/App'
import MenuComponent from '../vue/Widgets/Menu'
import SupportForm from '../vue/Widgets/SupportForm'
import { loadLangAsync, i18n } from './locale'
import { clearFormValidation, pick, startLoading, stopLoading } from './utils'
const routerState = {
afterLogin: null,
isLoggedIn: !!localStorage.getItem('token'),
isLocked: false
}
let loadingRoute
// Note: This has to be before the app is created
// Note: You cannot use app inside of the function
window.router.beforeEach((to, from, next) => {
// check if the route requires authentication and user is not logged in
if (to.meta.requiresAuth && !routerState.isLoggedIn) {
// remember the original request, to use after login
routerState.afterLogin = to;
// redirect to login page
next({ name: 'login' })
return
}
if (routerState.isLocked && to.meta.requiresAuth && !['login', 'payment-status'].includes(to.name)) {
// redirect to the payment-status page
next({ name: 'payment-status' })
return
}
if (to.meta.loading) {
startLoading()
loadingRoute = to.name
}
next()
})
window.router.afterEach((to, from) => {
if (to.name && loadingRoute === to.name) {
stopLoading()
loadingRoute = null
}
// When changing a page remove old:
// - error page
// - modal backdrop
$('#error-page,.modal-backdrop.show').remove()
$('body').css('padding', 0) // remove padding added by unclosed modal
// Close the mobile menu
if ($('#header-menu .navbar-collapse.show').length) {
$('#header-menu .navbar-toggler').click();
}
})
const app = new Vue({
components: {
AppComponent,
MenuComponent,
},
i18n,
router: window.router,
data() {
return {
authInfo: null,
isUser: !window.isAdmin && !window.isReseller,
appName: window.config['app.name'],
appUrl: window.config['app.url'],
themeDir: '/themes/' + window.config['app.theme']
}
},
methods: {
clearFormValidation,
countriesText(list) {
if (list && list.length) {
let result = []
list.forEach(code => {
let country = window.config.countries[code]
if (country) {
result.push(country[1])
} else {
console.warn(`Unknown country code: ${code}`)
}
})
return result.join(', ')
}
return this.$t('form.norestrictions')
},
hasPermission(type) {
const key = 'enable' + type.charAt(0).toUpperCase() + type.slice(1)
return !!(this.authInfo && this.authInfo.statusInfo[key])
},
hasRoute(name) {
return this.$router.resolve({ name: name }).resolved.matched.length > 0
},
hasSKU(name) {
return this.authInfo.statusInfo.skus && this.authInfo.statusInfo.skus.indexOf(name) != -1
},
isController(wallet_id) {
if (wallet_id && this.authInfo) {
let i
for (i = 0; i < this.authInfo.wallets.length; i++) {
if (wallet_id == this.authInfo.wallets[i].id) {
return true
}
}
for (i = 0; i < this.authInfo.accounts.length; i++) {
if (wallet_id == this.authInfo.accounts[i].id) {
return true
}
}
}
return false
},
isDegraded() {
return this.authInfo && this.authInfo.isAccountDegraded
},
// Set user state to "logged in"
loginUser(response, dashboard, update) {
if (!update) {
routerState.isLoggedIn = true
this.authInfo = null
}
localStorage.setItem('token', response.access_token)
localStorage.setItem('refreshToken', response.refresh_token)
if (response.email) {
this.authInfo = response
}
routerState.isLocked = this.authInfo && this.authInfo.isLocked
if (dashboard !== false) {
this.$router.push(routerState.afterLogin || { name: response.redirect || 'dashboard' })
} else if (routerState.isLocked && this.$route.meta.requiresAuth && this.$route.name != 'payment-status') {
// Always redirect locked user, here we can be after router's beforeEach handler
this.$router.push({ name: 'payment-status' })
}
routerState.afterLogin = null
// Refresh the token before it expires
let timeout = response.expires_in || 0
// We'll refresh 60 seconds before the token expires
if (timeout > 60) {
timeout -= 60
}
// TODO: We probably should try a few times in case of an error
// TODO: We probably should prevent axios from doing any requests
// while the token is being refreshed
this.refreshTimeout = setTimeout(() => {
axios.post('api/auth/refresh', { refresh_token: localStorage.getItem('refreshToken') }).then(response => {
this.loginUser(response.data, false, true)
})
}, timeout * 1000)
},
// Set user state to "not logged in"
logoutUser(redirect) {
routerState.isLoggedIn = true
this.authInfo = null
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
if (redirect !== false) {
this.$router.push({ name: 'login' })
}
clearTimeout(this.refreshTimeout)
},
logo(mode) {
let src = this.appUrl + this.themeDir + '/images/logo_' + (mode || 'header') + '.png'
return `<img src="${src}" alt="${this.appName}">`
},
pick,
startLoading,
stopLoading,
errorPage(code, msg, hint) {
// Until https://github.com/vuejs/vue-router/issues/977 is implemented
// we can't really use router to display error page as it has two side
// effects: it changes the URL and adds the error page to browser history.
// For now we'll be replacing current view with error page "manually".
if (!msg) msg = this.$te('error.' + code) ? this.$t('error.' + code) : this.$t('error.unknown')
if (!hint) hint = ''
const error_page = '<div id="error-page" class="error-page">'
+ `<div class="code">${code}</div><div class="message">${msg}</div><div class="hint">${hint}</div>`
+ '</div>'
$('#error-page').remove()
$('#app').append(error_page)
app.updateBodyClass('error')
},
errorHandler(error) {
stopLoading()
const status = error.response ? error.response.status : 500
const message = error.response ? error.response.statusText : ''
if (status == 401) {
// Remember requested route to come back to it after log in
if (this.$route.meta.requiresAuth) {
routerState.afterLogin = this.$route
this.logoutUser()
} else {
this.logoutUser(false)
}
} else {
if (!error.response) {
console.error(error)
}
this.errorPage(status, message)
}
},
price(price, currency) {
if (!currency) {
currency = 'CHF'
} else {
currency = currency.toUpperCase()
}
let args = { style: 'currency', currency }
if (currency == 'BTC') {
args.minimumFractionDigits = 6
args.maximumFractionDigits = 9
}
// TODO: Set locale argument according to the currently used locale
return ((price || 0) / 100).toLocaleString('de-DE', args)
},
priceLabel(cost, discount, currency) {
let index = ''
if (discount) {
cost = Math.floor(cost * ((100 - discount) / 100))
index = '\u00B9'
}
return this.price(cost, currency) + '/' + this.$t('wallet.month') + index
},
clickRecord(event) {
if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) {
$(event.target).closest('tr').find('a').trigger('click')
}
},
pageName(path) {
let page = this.$route.path
// check if it is a "menu page", find the page name
// otherwise we'll use the real path as page name
window.config.menu.every(item => {
if (item.location == page && item.page) {
page = item.page
return false
}
})
page = page.replace(/^\//, '')
return page ? page : '404'
},
supportDialog(container) {
let dialog = $('#support-dialog')[0]
if (!dialog) {
// FIXME: Find a nicer way of doing this
SupportForm.i18n = i18n
let form = new Vue(SupportForm)
form.$mount($('<div>').appendTo(container)[0])
form.$root = this
form.$toast = this.$toast
dialog = form.$el
}
dialog.__vue__.show()
},
statusClass(obj) {
if (obj.isDeleted) {
return 'text-muted'
}
if (obj.isDegraded || obj.isAccountDegraded || obj.isSuspended) {
return 'text-warning'
}
if (!obj.isReady) {
return 'text-danger'
}
return 'text-success'
},
statusText(obj) {
if (obj.isDeleted) {
return this.$t('status.deleted')
}
if (obj.isDegraded || obj.isAccountDegraded) {
return this.$t('status.degraded')
}
if (obj.isSuspended) {
return this.$t('status.suspended')
}
if (!obj.isReady) {
return this.$t('status.notready')
}
return this.$t('status.active')
},
unlock() {
routerState.isLocked = this.authInfo.isLocked = false
this.$router.push({ name: 'dashboard' })
},
// Append some wallet properties to the object
userWalletProps(object) {
let wallet = this.authInfo.accounts[0]
if (!wallet) {
wallet = this.authInfo.wallets[0]
}
if (wallet) {
object.currency = wallet.currency
if (wallet.discount) {
object.discount = wallet.discount
object.discount_description = wallet.discount_description
}
}
},
updateBodyClass(name) {
// Add 'class' attribute to the body, different for each page
// so, we can apply page-specific styles
document.body.className = 'page-' + (name || this.pageName()).replace(/\/.*$/, '').toLowerCase()
}
}
})
// Fetch the locale file and the start the app
loadLangAsync().then(() => app.$mount('#app'))
// Add a axios request interceptor
axios.interceptors.request.use(
config => {
- // This is the only way I found to change configuration options
- // on a running application. We need this for browser testing.
- config.headers['X-Test-Payment-Provider'] = window.config.paymentProvider
-
// Set the Authorization header. Note that some request might force
// empty Authorization header therefore we check if the header is already set,
// not whether it's empty
const token = localStorage.getItem('token')
if (token && !('Authorization' in config.headers)) {
config.headers.Authorization = 'Bearer ' + token
}
let loader = config.loader
if (loader) {
startLoading(loader)
}
return config
},
error => {
// Do something with request error
return Promise.reject(error)
}
)
// Add a axios response interceptor for general/validation error handler
axios.interceptors.response.use(
response => {
if (response.config.onFinish) {
response.config.onFinish()
}
let loader = response.config.loader
if (loader) {
stopLoading(loader)
}
return response
},
error => {
if (error.config && error.config.loader) {
stopLoading(error.config.loader)
}
// Do not display the error in a toast message, pass the error as-is
if (axios.isCancel(error) || (error.config && error.config.ignoreErrors)) {
return Promise.reject(error)
}
if (error.config && error.config.onFinish) {
error.config.onFinish()
}
let error_msg
const status = error.response ? error.response.status : 200
const data = error.response ? error.response.data : {}
if (status == 422 && data.errors) {
error_msg = app.$t('error.form')
const modal = $('div.modal.show')
$(modal.length ? modal : 'form').each((i, form) => {
form = $(form)
$.each(data.errors, (idx, msg) => {
const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx
let input = form.find('#' + input_name)
if (!input.length) {
input = form.find('[name="' + input_name + '"]');
}
if (input.length) {
// Create an error message
// API responses can use a string, array or object
let msg_text = ''
if (typeof(msg) !== 'string') {
$.each(msg, (index, str) => {
msg_text += str + ' '
})
}
else {
msg_text = msg
}
let feedback = $('<div class="invalid-feedback">').text(msg_text)
if (input.is('.list-input')) {
// List input widget
let controls = input.children(':not(:first-child)')
if (!controls.length && typeof msg == 'string') {
// this is an empty list (the main input only)
// and the error message is not an array
input.find('.main-input').addClass('is-invalid')
} else {
controls.each((index, element) => {
if (msg[index]) {
$(element).find('input').addClass('is-invalid')
}
})
}
input.addClass('is-invalid').next('.invalid-feedback').remove()
input.after(feedback)
} else {
// a special case, e.g. the invitation policy widget
if (input.is('select') && input.parent().is('.input-group-select.selected')) {
input = input.next()
}
// Standard form element
input.addClass('is-invalid')
input.parent().find('.invalid-feedback').remove()
input.parent().append(feedback)
}
}
})
form.find('.is-invalid:not(.list-input)').first().focus()
})
}
else if (data.status == 'error') {
error_msg = data.message
}
else {
error_msg = error.request ? error.request.statusText : error.message
}
app.$toast.error(error_msg || app.$t('error.server'))
// Pass the error as-is
return Promise.reject(error)
}
)
diff --git a/src/tests/Browser/PaymentCoinbaseTest.php b/src/tests/Browser/PaymentCoinbaseTest.php
index 30a87cd1..a7d53a7a 100644
--- a/src/tests/Browser/PaymentCoinbaseTest.php
+++ b/src/tests/Browser/PaymentCoinbaseTest.php
@@ -1,81 +1,81 @@
<?php
namespace Tests\Browser;
use App\Wallet;
use Tests\Browser;
use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\Wallet as WalletPage;
use Tests\TestCaseDusk;
class PaymentCoinbaseTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('payment-test@kolabnow.com');
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestUser('payment-test@kolabnow.com');
parent::tearDown();
}
/**
* Test the payment process
*
* @group coinbase
*/
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' => 'mollie'])
+ ->submitLogon('payment-test@kolabnow.com', 'simple123', true)
->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')
->waitFor('#payment-method-selection .link-bitcoin svg')
->click('#payment-method-selection .link-bitcoin');
})
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Top up your wallet')
->assertFocused('#amount')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Continue')
// Test error handling
->type('@body #amount', 'aaa')
->click('@button-action')
->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')
// Note we use double click to assert it does not create redundant requests
->click('@button-action')
->click('@button-action');
})
->waitUntilMissing('@payment-dialog');
$this->assertSame(1, $user->wallets()->first()->payments()->count());
});
}
}
diff --git a/src/tests/Browser/PaymentMollieTest.php b/src/tests/Browser/PaymentMollieTest.php
index 822e04ea..ca223260 100644
--- a/src/tests/Browser/PaymentMollieTest.php
+++ b/src/tests/Browser/PaymentMollieTest.php
@@ -1,302 +1,304 @@
<?php
namespace Tests\Browser;
use App\Payment;
use App\Wallet;
use Tests\Browser;
use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\PaymentMollie;
use Tests\Browser\Pages\Wallet as WalletPage;
use Tests\TestCaseDusk;
class PaymentMollieTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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) use ($user) {
- $browser->visit(new Home())
- ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'mollie'])
+ $browser->withConfig(['services.payment_provider' => 'mollie'])
+ ->visit(new Home())
+ ->submitLogon('payment-test@kolabnow.com', 'simple123', true)
->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')
->waitFor('#payment-method-selection .link-creditcard svg')
->waitFor('#payment-method-selection .link-paypal svg')
->waitFor('#payment-method-selection .link-banktransfer svg')
->click('#payment-method-selection .link-creditcard');
})
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Top up your wallet')
->assertFocused('#amount')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Continue')
// Test error handling
->type('@body #amount', 'aaa')
->click('@button-action')
->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')
// Note we use double click to assert it does not create redundant requests
->click('@button-action')
->click('@button-action');
})
->on(new PaymentMollie())
->assertSeeIn('@title', $user->tenant->title . ' Payment')
->assertSeeIn('@amount', 'CHF 12.34')
->submitPayment()
->waitForLocation('/wallet')
->on(new WalletPage())
->assertSeeIn('@main .card-title', 'Account balance 12,34 CHF');
$this->assertSame(1, $user->wallets()->first()->payments()->count());
});
}
/**
* 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) use ($user) {
- $browser->visit(new Home())
- ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'mollie'])
+ $browser->withConfig(['services.payment_provider' => 'mollie'])
+ ->visit(new Home())
+ ->submitLogon('payment-test@kolabnow.com', 'simple123', true)
->on(new Dashboard())
->click('@links .link-wallet')
->on(new WalletPage())
->assertMissing('@body #mandate-form .alert')
->click('@main #mandate-form button')
/*
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('#payment-method-selection .link-creditcard svg')
->assertMissing('#payment-method-selection .link-paypal')
->assertMissing('#payment-method-selection .link-banktransfer')
->click('#payment-method-selection .link-creditcard');
})
*/
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('@body #mandate_amount')
->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by')
->assertValue('@body #mandate_amount', Payment::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')
// Note we use double click to assert it does not create redundant requests
->click('@button-action')
->click('@button-action');
})
->on(new PaymentMollie())
->assertSeeIn('@title', $user->tenant->title . ' Auto-Payment Setup')
->assertMissing('@amount')
->submitPayment()
->waitForLocation('/wallet')
- ->visit('/wallet?paymentProvider=mollie')
+ ->visit('/wallet')
->waitFor('#mandate-info')
->assertPresent('#mandate-info p:first-child')
->assertSeeIn(
'#mandate-info p:first-child',
'Auto-payment is set to fill up your account by 100 CHF ' .
'every time your account balance gets under 0 CHF.'
)
->assertSeeIn(
'#mandate-info p:nth-child(2)',
'Mastercard (**** **** **** 9399)'
)
->assertMissing('@body .alert');
$this->assertSame(1, $user->wallets()->first()->payments()->count());
});
// Test updating (disabled) auto-payment
$this->browse(function (Browser $browser) use ($user) {
$wallet = $user->wallets()->first();
$wallet->setSetting('mandate_disabled', 1);
$browser->refresh()
->on(new WalletPage())
->waitFor('#mandate-info')
->assertSeeIn(
'#mandate-info .disabled-mandate',
'The configured auto-payment has been disabled'
)
->assertSeeIn('#mandate-info button.btn-primary', 'Change auto-payment')
->click('#mandate-info button.btn-primary')
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Update auto-payment')
->assertSeeIn(
'@body form .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.')
// make sure the "disabled" text isn't there
->assertMissing('#mandate-info .disabled-mandate')
->click('#mandate-info button.btn-primary')
->assertMissing('form .disabled-mandate')
->click('button.modal-cancel');
});
// Test deleting auto-payment
$this->browse(function (Browser $browser) {
$browser->on(new WalletPage())
->waitFor('#mandate-info')
->assertSeeIn('#mandate-info * button.btn-danger', 'Cancel auto-payment')
->assertVisible('#mandate-info * button.btn-danger')
->click('#mandate-info * button.btn-danger')
->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been removed.')
->assertVisible('#mandate-form')
->assertMissing('#mandate-info');
});
// Test pending and failed mandate
$this->browse(function (Browser $browser) {
$browser->on(new WalletPage())
->assertMissing('@body #mandate-form .alert')
->click('@main #mandate-form button')
/*
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('#payment-method-selection .link-creditcard')
->click('#payment-method-selection .link-creditcard');
})
*/
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('@body #mandate_amount')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Continue')
// Submit valid data
->type('@body #mandate_amount', '100')
->type('@body #mandate_balance', '0')
->click('@button-action');
})
->on(new PaymentMollie())
->submitPayment('open')
->waitForLocation('/wallet')
- ->visit('/wallet?paymentProvider=mollie')
+ ->visit('/wallet')
->on(new WalletPage())
->assertSeeIn(
'#mandate-info .alert-warning',
'The setup of the automatic payment is still in progress.'
)
// Delete the mandate
->click('#mandate-info * button.btn-danger')
->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been removed.')
->assertMissing('@body #mandate-form .alert')
// Create a new mandate
->click('@main #mandate-form button')
/*
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('#payment-method-selection .link-creditcard')
->click('#payment-method-selection .link-creditcard');
})
*/
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('@body #mandate_amount')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Continue')
// Submit valid data
->type('@body #mandate_amount', '100')
->type('@body #mandate_balance', '0')
->click('@button-action');
})
->on(new PaymentMollie())
->submitPayment('failed')
->waitForLocation('/wallet')
- ->visit('/wallet?paymentProvider=mollie')
+ ->visit('/wallet')
->on(new WalletPage())
->waitFor('#mandate-form .alert-danger')
->assertSeeIn(
'#mandate-form .alert-danger',
'The setup of automatic payments failed. Restart the process to enable'
)
->click('@main button')
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->waitFor('#mandate-form')
->assertMissing('#mandate-info');
});
});
}
}
diff --git a/src/tests/Browser/PaymentStripeTest.php b/src/tests/Browser/PaymentStripeTest.php
index ba797203..5baa7005 100644
--- a/src/tests/Browser/PaymentStripeTest.php
+++ b/src/tests/Browser/PaymentStripeTest.php
@@ -1,237 +1,239 @@
<?php
namespace Tests\Browser;
use App\Payment;
use App\Wallet;
use Tests\Browser;
use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\PaymentStripe;
use Tests\Browser\Pages\Wallet as WalletPage;
use Tests\TestCaseDusk;
class PaymentStripeTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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'])
+ $browser->withConfig(['services.payment_provider' => 'stripe'])
+ ->visit(new Home())
+ ->submitLogon('payment-test@kolabnow.com', 'simple123', true)
->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')
->waitFor('#payment-method-selection .link-creditcard svg')
->waitFor('#payment-method-selection .link-paypal svg')
->assertMissing('#payment-method-selection .link-banktransfer svg')
->click('#payment-method-selection .link-creditcard');
})
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Top up your wallet')
->assertFocused('#amount')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Continue')
// Test error handling
->type('@body #amount', 'aaa')
->click('@button-action')
->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')
// Note we use double click to assert it does not create redundant requests
->click('@button-action')
->click('@button-action');
})
->on(new PaymentStripe())
->assertSeeIn('@title', $user->tenant->title . ' Payment')
->assertSeeIn('@amount', 'CHF 12.34')
->assertSeeIn('@email', $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-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'])
+ $browser->withConfig(['services.payment_provider' => 'stripe'])
+ ->visit(new Home())
+ ->submitLogon('payment-test@kolabnow.com', 'simple123', true)
->on(new Dashboard())
->click('@links .link-wallet')
->on(new WalletPage())
->assertMissing('@body #mandate-form .alert')
->click('@main #mandate-form button')
/*
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('#payment-method-selection .link-creditcard')
->assertMissing('#payment-method-selection .link-paypal')
->assertMissing('#payment-method-selection .link-banktransfer')
->click('#payment-method-selection .link-creditcard');
})
*/
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Set up auto-payment')
->waitFor('@body #mandate_amount')
->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by')
->assertValue('@body #mandate_amount', Payment::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')
// Note we use double click to assert it does not create redundant requests
->click('@button-action')
->click('@button-action');
})
->on(new PaymentStripe())
->assertMissing('@title')
->assertMissing('@amount')
->assertSeeIn('@email', $user->email)
->submitValidCreditCard()
->waitForLocation('/wallet', 30) // need more time than default 5 sec.
- ->visit('/wallet?paymentProvider=stripe')
+ ->visit('/wallet')
->waitFor('#mandate-info')
->assertPresent('#mandate-info p:first-child')
->assertSeeIn(
'#mandate-info p:first-child',
'Auto-payment is set to fill up your account by 100 CHF ' .
'every time your account balance gets under 0 CHF.'
)
->assertSeeIn(
'#mandate-info p:nth-child(2)',
'Visa (**** **** **** 4242)'
)
->assertMissing('@body .alert');
});
// Test updating (disabled) auto-payment
$this->browse(function (Browser $browser) use ($user) {
$wallet = $user->wallets()->first();
$wallet->setSetting('mandate_disabled', 1);
$browser->refresh()
->on(new WalletPage())
->waitFor('#mandate-info')
->assertSeeIn(
'#mandate-info .disabled-mandate',
'The configured auto-payment has been disabled'
)
->assertSeeIn('#mandate-info button.btn-primary', 'Change auto-payment')
->click('#mandate-info button.btn-primary')
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Update auto-payment')
->assertSeeIn(
'@body form .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.')
// make sure the "disabled" text isn't there
->assertMissing('#mandate-info .disabled-mandate')
->click('#mandate-info button.btn-primary')
->assertMissing('form .disabled-mandate')
->click('button.modal-cancel');
});
// Test deleting auto-payment
$this->browse(function (Browser $browser) {
$browser->on(new WalletPage())
->waitFor('#mandate-info')
->assertSeeIn('#mandate-info * button.btn-danger', 'Cancel auto-payment')
->assertVisible('#mandate-info * button.btn-danger')
->click('#mandate-info * button.btn-danger')
->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been removed.')
->assertVisible('#mandate-form')
->assertMissing('#mandate-info');
});
}
}
diff --git a/src/tests/Browser/Reseller/PaymentMollieTest.php b/src/tests/Browser/Reseller/PaymentMollieTest.php
index 5ccff4c3..d52af8d3 100644
--- a/src/tests/Browser/Reseller/PaymentMollieTest.php
+++ b/src/tests/Browser/Reseller/PaymentMollieTest.php
@@ -1,95 +1,96 @@
<?php
namespace Tests\Browser\Reseller;
use App\Wallet;
use Tests\Browser;
use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\PaymentMollie;
use Tests\Browser\Pages\Wallet as WalletPage;
use Tests\TestCaseDusk;
class PaymentMollieTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useResellerUrl();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$user = $this->getTestUser('reseller@' . \config('app.domain'));
$wallet = $user->wallets()->first();
$wallet->payments()->delete();
$wallet->balance = 0;
$wallet->save();
parent::tearDown();
}
/**
* Test the payment process
*
* @group mollie
*/
public function testPayment(): void
{
$this->browse(function (Browser $browser) {
$user = $this->getTestUser('reseller@' . \config('app.domain'));
$wallet = $user->wallets()->first();
$wallet->payments()->delete();
$wallet->balance = 0;
$wallet->save();
- $browser->visit(new Home())
- ->submitLogon($user->email, \App\Utils::generatePassphrase(), true, ['paymentProvider' => 'mollie'])
+ $browser->withConfig(['services.payment_provider' => 'mollie'])
+ ->visit(new Home())
+ ->submitLogon($user->email, \App\Utils::generatePassphrase(), true)
->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')
->waitFor('#payment-method-selection .link-creditcard svg')
->waitFor('#payment-method-selection .link-paypal svg')
->waitFor('#payment-method-selection .link-banktransfer svg')
->click('#payment-method-selection .link-creditcard');
})
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Top up your wallet')
->assertFocused('#amount')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Continue')
// Test error handling
->type('@body #amount', 'aaa')
->click('@button-action')
->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')
// Note we use double click to assert it does not create redundant requests
->click('@button-action')
->click('@button-action');
})
->on(new PaymentMollie())
->assertSeeIn('@title', $user->tenant->title . ' Payment')
->assertSeeIn('@amount', 'CHF 12.34')
->submitPayment()
->waitForLocation('/wallet')
->on(new WalletPage())
->assertSeeIn('@main .card-title', 'Account balance 12,34 CHF');
$this->assertSame(1, $wallet->payments()->count());
});
}
}
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
index 9c53634f..d7a5b622 100644
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -1,864 +1,862 @@
<?php
namespace Tests\Browser;
use App\Discount;
use App\Domain;
use App\Plan;
use App\SignupCode;
use App\SignupInvitation;
use App\User;
use Tests\Browser;
use Tests\Browser\Components\Menu;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\PaymentMollie;
use Tests\Browser\Pages\PaymentStatus;
use Tests\Browser\Pages\Signup;
use Tests\TestCaseDusk;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class SignupTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('signuptestdusk@' . \config('app.domain'));
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
Plan::whereNot('mode', Plan::MODE_EMAIL)->update(['mode' => Plan::MODE_EMAIL]);
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestUser('signuptestdusk@' . \config('app.domain'));
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
SignupInvitation::truncate();
Plan::whereNot('mode', Plan::MODE_EMAIL)->update(['mode' => Plan::MODE_EMAIL]);
@unlink(storage_path('signup-tokens.txt'));
parent::tearDown();
}
/**
* Test signup code verification with a link
*/
public function testSignupCodeByLink(): void
{
// Test invalid code (invalid format)
$this->browse(function (Browser $browser) {
// Register Signup page element selectors we'll be using
$browser->onWithoutAssert(new Signup());
// TODO: Test what happens if user is logged in
$browser->visit('/signup/invalid-code');
// TODO: According to https://github.com/vuejs/vue-router/issues/977
// it is not yet easily possible to display error page component (route)
// without changing the URL
// TODO: Instead of css selector we should probably define page/component
// and use it instead
$browser->waitFor('#error-page');
});
// Test invalid code (valid format)
$this->browse(function (Browser $browser) {
$browser->visit('/signup/XXXXX-code');
// FIXME: User will not be able to continue anyway, so we should
// either display 1st step or 404 error page
$browser->waitFor('@step1')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Test valid code
$this->browse(function (Browser $browser) {
$code = SignupCode::create([
'email' => 'User@example.org',
'first_name' => 'User',
'last_name' => 'Name',
'plan' => 'individual',
'voucher' => '',
]);
$browser->visit('/signup/' . $code->short_code . '-' . $code->code)
->waitFor('@step3')
->assertMissing('@step1')
->assertMissing('@step2');
// FIXME: Find a nice way to read javascript data without using hidden inputs
$this->assertSame($code->code, $browser->value('@step2 #signup_code'));
// TODO: Test if the signup process can be completed
});
}
/**
* Test signup "welcome" page
*/
public function testSignupStep0(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Signup());
$browser->assertVisible('@step0')
->assertMissing('@step1')
->assertMissing('@step2')
->assertMissing('@step3');
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['support', 'signup', 'login', 'lang'], 'signup');
});
$browser->waitFor('@step0 .plan-selector .card');
// Assert first plan box and press the button
$browser->with('@step0 .plan-selector .plan-individual', function ($step) {
$step->assertVisible('button')
->assertSeeIn('button', 'Individual Account')
->assertVisible('.plan-description')
->click('button');
});
$browser->waitForLocation('/signup/individual')
->assertVisible('@step1')
->assertSeeIn('.card-title', 'Sign Up - Step 1/3')
->assertMissing('@step0')
->assertMissing('@step2')
->assertMissing('@step3')
->assertFocused('@step1 #signup_first_name');
// Click Back button
$browser->click('@step1 [type=button]')
->waitForLocation('/signup')
->assertVisible('@step0')
->assertMissing('@step1')
->assertMissing('@step2')
->assertMissing('@step3');
// Choose the group account plan
$browser->click('@step0 .plan-selector .plan-group button')
->waitForLocation('/signup/group')
->assertVisible('@step1')
->assertMissing('@step0')
->assertMissing('@step2')
->assertMissing('@step3')
->assertFocused('@step1 #signup_first_name');
// TODO: Test if 'plan' variable is set properly in vue component
});
}
/**
* Test 1st step of the signup process
*/
public function testSignupStep1(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/signup/individual')
->onWithoutAssert(new Signup());
// Here we expect two text inputs and Back and Continue buttons
$browser->with('@step1', function ($step) {
$step->waitFor('#signup_last_name')
->assertSeeIn('.card-title', 'Sign Up - Step 1/3')
->assertVisible('#signup_first_name')
->assertFocused('#signup_first_name')
->assertVisible('#signup_email')
->assertVisible('[type=button]')
->assertVisible('[type=submit]');
});
// Submit empty form
// Email is required, so after pressing Submit
// we expect focus to be moved to the email input
$browser->with('@step1', function ($step) {
$step->click('[type=submit]');
$step->assertFocused('#signup_email');
});
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['support', 'signup', 'login', 'lang'], 'signup');
});
// Submit invalid email, and first_name
// We expect both inputs to have is-invalid class added, with .invalid-feedback element
$browser->with('@step1', function ($step) {
$step->type('#signup_first_name', str_repeat('a', 250))
->type('#signup_email', '@test')
->click('[type=submit]')
->waitFor('#signup_email.is-invalid')
->assertVisible('#signup_first_name.is-invalid')
->assertVisible('#signup_email + .invalid-feedback')
->assertVisible('#signup_last_name + .invalid-feedback')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid data
// We expect error state on email input to be removed, and Step 2 form visible
$browser->with('@step1', function ($step) {
$step->type('#signup_first_name', 'Test')
->type('#signup_last_name', 'User')
->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
->click('[type=submit]')
->assertMissing('#signup_email.is-invalid')
->assertMissing('#signup_email + .invalid-feedback');
});
$browser->waitUntilMissing('@step2 #signup_code[value=""]');
$browser->waitFor('@step2');
$browser->assertMissing('@step1');
});
}
/**
* Test 2nd Step of the signup process
*
* @depends testSignupStep1
*/
public function testSignupStep2(): void
{
$this->browse(function (Browser $browser) {
$browser->assertVisible('@step2')
->assertSeeIn('@step2 .card-title', 'Sign Up - Step 2/3')
->assertMissing('@step0')
->assertMissing('@step1')
->assertMissing('@step3');
// Here we expect one text input, Back and Continue buttons
$browser->with('@step2', function ($step) {
$step->assertVisible('#signup_short_code')
->assertFocused('#signup_short_code')
->assertVisible('[type=button]')
->assertVisible('[type=submit]');
});
// Test Back button functionality
$browser->click('@step2 [type=button]')
->waitFor('@step1')
->assertFocused('@step1 #signup_first_name')
->assertMissing('@step2');
// Submit valid Step 1 data (again)
$browser->with('@step1', function ($step) {
$step->type('#signup_first_name', 'User')
->type('#signup_last_name', 'User')
->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
->click('[type=submit]');
});
$browser->waitFor('@step2');
$browser->assertMissing('@step1');
// Submit invalid code
// We expect code input to have is-invalid class added, with .invalid-feedback element
$browser->with('@step2', function ($step) {
$step->type('#signup_short_code', 'XXXXX');
$step->click('[type=submit]');
$step->waitFor('#signup_short_code.is-invalid')
->assertVisible('#signup_short_code + .invalid-feedback')
->assertFocused('#signup_short_code')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid code
// We expect error state on code input to be removed, and Step 3 form visible
$browser->with('@step2', function ($step) {
// Get the code and short_code from database
// FIXME: Find a nice way to read javascript data without using hidden inputs
$code = $step->value('#signup_code');
$this->assertNotEmpty($code);
$code = SignupCode::find($code);
$step->type('#signup_short_code', $code->short_code);
$step->click('[type=submit]');
$step->assertMissing('#signup_short_code.is-invalid');
$step->assertMissing('#signup_short_code + .invalid-feedback');
});
$browser->waitFor('@step3');
$browser->assertMissing('@step2');
});
}
/**
* Test 3rd Step of the signup process
*
* @depends testSignupStep2
*/
public function testSignupStep3(): void
{
$this->browse(function (Browser $browser) {
$browser->assertVisible('@step3');
// Here we expect 3 text inputs, Back and Continue buttons
$browser->with('@step3', function ($step) {
$domains = Domain::getPublicDomains();
$domains_count = count($domains);
$step->assertSeeIn('.card-title', 'Sign Up - Step 3/3')
->assertMissing('#signup_last_name')
->assertMissing('#signup_first_name')
->assertVisible('#signup_login')
->assertVisible('#signup_password')
->assertVisible('#signup_password_confirmation')
->assertVisible('select#signup_domain')
->assertElementsCount('select#signup_domain option', $domains_count, false)
->assertText('select#signup_domain option:nth-child(1)', $domains[0])
->assertValue('select#signup_domain option:nth-child(1)', $domains[0])
->assertText('select#signup_domain option:nth-child(2)', $domains[1])
->assertValue('select#signup_domain option:nth-child(2)', $domains[1])
->assertVisible('[type=button]')
->assertVisible('[type=submit]')
->assertSeeIn('[type=submit]', 'Submit')
->assertFocused('#signup_login')
->assertSelected('select#signup_domain', \config('app.domain'))
->assertValue('#signup_login', '')
->assertValue('#signup_password', '')
->assertValue('#signup_password_confirmation', '')
->with('#signup_password_policy', function (Browser $browser) {
$browser->assertElementsCount('li', 2)
->assertMissing('li:first-child svg.text-success')
->assertSeeIn('li:first-child small', "Minimum password length: 6 characters")
->assertMissing('li:last-child svg.text-success')
->assertSeeIn('li:last-child small', "Maximum password length: 255 characters");
});
// TODO: Test domain selector
});
// Test Back button
$browser->click('@step3 [type=button]');
$browser->waitFor('@step2');
$browser->assertFocused('@step2 #signup_short_code');
$browser->assertMissing('@step3');
// TODO: Test form reset when going back
// Submit valid code again
$browser->with('@step2', function ($step) {
$code = $step->value('#signup_code');
$this->assertNotEmpty($code);
$code = SignupCode::find($code);
$step->type('#signup_short_code', $code->short_code);
$step->click('[type=submit]');
});
$browser->waitFor('@step3');
// Submit invalid data
$browser->with('@step3', function ($step) {
$step->assertFocused('#signup_login')
->type('#signup_login', '*')
->type('#signup_password', '12345678')
->type('#signup_password_confirmation', '123456789')
->with('#signup_password_policy', function (Browser $browser) {
$browser->waitFor('li:first-child svg.text-success')
->waitFor('li:last-child svg.text-success');
})
->click('[type=submit]')
->waitFor('#signup_login.is-invalid')
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password_input .invalid-feedback')
->assertFocused('#signup_login')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid data (valid login, invalid password)
$browser->with('@step3', function ($step) {
$step->type('#signup_login', 'SignupTestDusk')
->click('[type=submit]')
->waitFor('#signup_password.is-invalid')
->assertVisible('#signup_password_input .invalid-feedback')
->assertMissing('#signup_login.is-invalid')
->assertMissing('#signup_domain + .invalid-feedback')
->assertFocused('#signup_password')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid data
$browser->with('@step3', function ($step) {
$step->type('#signup_password_confirmation', '12345678');
$step->click('[type=submit]');
});
// At this point we should be auto-logged-in to dashboard
$browser->waitUntilMissing('@step3')
->waitUntilMissing('.app-loader')
->on(new Dashboard())
->assertUser('signuptestdusk@' . \config('app.domain'))
->assertVisible('@links a.link-profile')
->assertMissing('@links a.link-domains')
->assertVisible('@links a.link-users')
->assertVisible('@links a.link-wallet');
// Logout the user
$browser->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
});
}
/**
* Test signup for a group account
*/
public function testSignupGroup(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Signup());
// Choose the group account plan
$browser->waitFor('@step0 .plan-group button')
->click('@step0 .plan-group button');
// Submit valid data
// We expect error state on email input to be removed, and Step 2 form visible
$browser->whenAvailable('@step1', function ($step) {
$step->type('#signup_first_name', 'Test')
->type('#signup_last_name', 'User')
->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
->click('[type=submit]');
});
// Submit valid code
$browser->whenAvailable('@step2', function ($step) {
// Get the code and short_code from database
// FIXME: Find a nice way to read javascript data without using hidden inputs
$code = $step->value('#signup_code');
$code = SignupCode::find($code);
$step->type('#signup_short_code', $code->short_code)
->click('[type=submit]');
});
// Here we expect 4 text inputs, Back and Continue buttons
$browser->whenAvailable('@step3', function ($step) {
$step->assertVisible('#signup_login')
->assertVisible('#signup_password')
->assertVisible('#signup_password_confirmation')
->assertVisible('input#signup_domain')
->assertVisible('[type=button]')
->assertVisible('[type=submit]')
->assertFocused('#signup_login')
->assertValue('input#signup_domain', '')
->assertValue('#signup_login', '')
->assertValue('#signup_password', '')
->assertValue('#signup_password_confirmation', '');
});
// Submit invalid login and password data
$browser->with('@step3', function ($step) {
$step->assertFocused('#signup_login')
->type('#signup_login', '*')
->type('#signup_domain', 'test.com')
->type('#signup_password', '12345678')
->type('#signup_password_confirmation', '123456789')
->click('[type=submit]')
->waitFor('#signup_login.is-invalid')
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password_input .invalid-feedback')
->assertFocused('#signup_login')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid domain
$browser->with('@step3', function ($step) {
$step->type('#signup_login', 'admin')
->type('#signup_domain', 'aaa')
->type('#signup_password', '12345678')
->type('#signup_password_confirmation', '12345678')
->click('[type=submit]')
->waitUntilMissing('#signup_login.is-invalid')
->waitFor('#signup_domain.is-invalid + .invalid-feedback')
->assertMissing('#signup_password.is-invalid')
->assertMissing('#signup_password_input .invalid-feedback')
->assertFocused('#signup_domain')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid domain
$browser->with('@step3', function ($step) {
$step->type('#signup_domain', 'user-domain-signup.com')
->click('[type=submit]');
});
// At this point we should be auto-logged-in to dashboard
$browser->waitUntilMissing('@step3')
->waitUntilMissing('.app-loader')
->on(new Dashboard())
->assertUser('admin@user-domain-signup.com')
->assertVisible('@links a.link-profile')
->assertVisible('@links a.link-domains')
->assertVisible('@links a.link-users')
->assertVisible('@links a.link-wallet');
$browser->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
});
}
/**
* Test signup with a mandate plan, also the UI lock
*
* @group mollie
*/
public function testSignupMandate(): void
{
// Test the individual plan
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
$plan->mode = Plan::MODE_MANDATE;
$plan->save();
$this->browse(function (Browser $browser) {
- $config = ['paymentProvider' => 'mollie'];
- $browser->visit(new Signup())
- // Force Mollie
- ->execScript(sprintf('Object.assign(window.config, %s)', \json_encode($config)))
+ $browser->withConfig(['services.payment_provider' => 'mollie'])
+ ->visit(new Signup())
->waitFor('@step0 .plan-individual button')
->click('@step0 .plan-individual button')
// Test Back button
->whenAvailable('@step3', function ($browser) {
$browser->click('button[type=button]');
})
->whenAvailable('@step0', function ($browser) {
$browser->click('.plan-individual button');
})
// Test submit
->whenAvailable('@step3', function ($browser) {
$domains = Domain::getPublicDomains();
$domains_count = count($domains);
$browser->assertMissing('.card-title')
->assertElementsCount('select#signup_domain option', $domains_count, false)
->assertText('select#signup_domain option:nth-child(1)', $domains[0])
->assertValue('select#signup_domain option:nth-child(1)', $domains[0])
->type('#signup_login', 'signuptestdusk')
->type('#signup_password', '12345678')
->type('#signup_password_confirmation', '12345678')
->click('[type=submit]');
})
->whenAvailable('@step4', function ($browser) {
$browser->assertSeeIn('h4', 'The account is about to be created!')
->assertSeeIn('h5', 'You are choosing a monthly subscription')
->assertVisible('#summary-content')
->assertElementsCount('#summary-cc svg', 2)
->assertElementsCount('#summary-summary tr', 4)
->assertSeeIn('button.btn-primary', 'Continue')
->assertSeeIn('button.btn-secondary', 'Back')
->click('button.btn-secondary');
})
->whenAvailable('@step3', function ($browser) {
$browser->assertValue('#signup_login', 'signuptestdusk')
->click('[type=submit]');
})
->whenAvailable('@step4', function ($browser) {
$browser->click('button.btn-primary');
})
->on(new PaymentMollie())
->assertSeeIn('@title', 'Auto-Payment Setup')
->assertMissing('@amount')
->submitPayment('open')
->on(new PaymentStatus())
->assertSeeIn('@lock-alert', 'The account is locked')
->assertSeeIn('@content', 'Checking the status...')
->assertSeeIn('@button', 'Try again');
});
$user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first();
$this->assertSame($plan->id, $user->getSetting('plan_id'));
$this->assertFalse($user->isActive());
// Refresh and see that the account is still locked
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/dashboard')
->on(new PaymentStatus())
->assertSeeIn('@lock-alert', 'The account is locked')
->assertSeeIn('@content', 'Checking the status...');
// Mark the payment paid, and activate the user in background,
// expect unlock and redirect to the dashboard
// TODO: Move this to a separate tests file for PaymentStatus page
$payment = $user->wallets()->first()->payments()->first();
$payment->credit('Test');
$payment->status = \App\Payment::STATUS_PAID;
$payment->save();
$this->assertTrue($user->fresh()->isActive());
$browser->waitForLocation('/dashboard', 10)
->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
});
// TODO: Test the 'Try again' button on /payment/status page
}
/**
* Test signup with a token plan
*/
public function testSignupToken(): void
{
// Test the individual plan
Plan::where('title', 'individual')->update(['mode' => Plan::MODE_TOKEN]);
// Register some valid tokens
$tokens = ['1234567890', 'abcdefghijk'];
file_put_contents(storage_path('signup-tokens.txt'), implode("\n", $tokens));
$this->browse(function (Browser $browser) use ($tokens) {
$browser->visit(new Signup())
->waitFor('@step0 .plan-individual button')
->click('@step0 .plan-individual button')
// Step 1
->whenAvailable('@step1', function ($browser) use ($tokens) {
$browser->assertSeeIn('.card-title', 'Sign Up - Step 1/2')
->type('#signup_first_name', 'Test')
->type('#signup_last_name', 'User')
->assertMissing('#signup_email')
->type('#signup_token', '1234')
// invalid token
->click('[type=submit]')
->waitFor('#signup_token.is-invalid')
->assertVisible('#signup_token + .invalid-feedback')
->assertFocused('#signup_token')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
// valid token
->type('#signup_token', $tokens[0])
->click('[type=submit]');
})
// Step 2
->whenAvailable('@step3', function ($browser) {
$domains = Domain::getPublicDomains();
$domains_count = count($domains);
$browser->assertSeeIn('.card-title', 'Sign Up - Step 2/2')
->assertElementsCount('select#signup_domain option', $domains_count, false)
->assertText('select#signup_domain option:nth-child(1)', $domains[0])
->assertValue('select#signup_domain option:nth-child(1)', $domains[0])
->type('#signup_login', 'signuptestdusk')
->type('#signup_password', '12345678')
->type('#signup_password_confirmation', '12345678')
->click('[type=submit]');
})
->waitUntilMissing('@step3')
->on(new Dashboard())
->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
});
$user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first();
$this->assertSame($tokens[0], $user->getSetting('signup_token'));
$this->assertSame(null, $user->getSetting('external_email'));
// Test the group plan
Plan::where('title', 'group')->update(['mode' => Plan::MODE_TOKEN]);
$this->browse(function (Browser $browser) use ($tokens) {
$browser->visit(new Signup())
->waitFor('@step0 .plan-group button')
->click('@step0 .plan-group button')
// Step 1
->whenAvailable('@step1', function ($browser) use ($tokens) {
$browser->assertSeeIn('.card-title', 'Sign Up - Step 1/2')
->type('#signup_first_name', 'Test')
->type('#signup_last_name', 'User')
->assertMissing('#signup_email')
->type('#signup_token', '1234')
// invalid token
->click('[type=submit]')
->waitFor('#signup_token.is-invalid')
->assertVisible('#signup_token + .invalid-feedback')
->assertFocused('#signup_token')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
// valid token
->type('#signup_token', $tokens[1])
->click('[type=submit]');
})
// Step 2
->whenAvailable('@step3', function ($browser) {
$browser->assertSeeIn('.card-title', 'Sign Up - Step 2/2')
->type('input#signup_domain', 'user-domain-signup.com')
->type('#signup_login', 'admin')
->type('#signup_password', '12345678')
->type('#signup_password_confirmation', '12345678')
->click('[type=submit]');
})
->waitUntilMissing('@step3')
->on(new Dashboard())
->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
});
$user = User::where('email', 'admin@user-domain-signup.com')->first();
$this->assertSame($tokens[1], $user->getSetting('signup_token'));
$this->assertSame(null, $user->getSetting('external_email'));
}
/**
* Test signup with voucher
*/
public function testSignupVoucherLink(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/signup/voucher/TEST')
->onWithoutAssert(new Signup())
->waitUntilMissing('.app-loader')
->waitFor('@step0')
->click('.plan-individual button')
->whenAvailable('@step1', function (Browser $browser) {
$browser->type('#signup_first_name', 'Test')
->type('#signup_last_name', 'User')
->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
->click('[type=submit]');
})
->whenAvailable('@step2', function (Browser $browser) {
// Get the code and short_code from database
// FIXME: Find a nice way to read javascript data without using hidden inputs
$code = $browser->value('#signup_code');
$this->assertNotEmpty($code);
$code = SignupCode::find($code);
$browser->type('#signup_short_code', $code->short_code)
->click('[type=submit]');
})
->whenAvailable('@step3', function (Browser $browser) {
// Assert that the code is filled in the input
// Change it and test error handling
$browser->assertValue('#signup_voucher', 'TEST')
->type('#signup_voucher', 'TESTXX')
->type('#signup_login', 'signuptestdusk')
->type('#signup_password', '123456789')
->type('#signup_password_confirmation', '123456789')
->click('[type=submit]')
->waitFor('#signup_voucher.is-invalid')
->assertVisible('#signup_voucher + .invalid-feedback')
->assertFocused('#signup_voucher')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
// Submit the correct code
->type('#signup_voucher', 'TEST')
->click('[type=submit]');
})
->waitUntilMissing('@step3')
->waitUntilMissing('.app-loader')
->on(new Dashboard())
->assertUser('signuptestdusk@' . \config('app.domain'))
// Logout the user
->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
});
$user = $this->getTestUser('signuptestdusk@' . \config('app.domain'));
$discount = Discount::where('code', 'TEST')->first();
$this->assertSame($discount->id, $user->wallets()->first()->discount_id);
}
/**
* Test signup via invitation link
*/
public function testSignupInvitation(): void
{
// Test non-existing invitation
$this->browse(function (Browser $browser) {
$browser->visit('/signup/invite/TEST')
->onWithoutAssert(new Signup())
->waitFor('#app > #error-page')
->assertErrorPage(404);
});
$invitation = SignupInvitation::create(['email' => 'test@domain.org']);
$this->browse(function (Browser $browser) use ($invitation) {
$browser->visit('/signup/invite/' . $invitation->id)
->onWithoutAssert(new Signup())
->waitUntilMissing('.app-loader')
->with('@step3', function ($step) {
$domains_count = count(Domain::getPublicDomains());
$step->assertMissing('.card-title')
->assertVisible('#signup_last_name')
->assertVisible('#signup_first_name')
->assertVisible('#signup_login')
->assertVisible('#signup_password')
->assertVisible('#signup_password_confirmation')
->assertVisible('select#signup_domain')
->assertElementsCount('select#signup_domain option', $domains_count, false)
->assertVisible('[type=submit]')
->assertMissing('[type=button]') // Back button
->assertSeeIn('[type=submit]', 'Sign Up')
->assertFocused('#signup_first_name')
->assertValue('select#signup_domain', \config('app.domain'))
->assertValue('#signup_first_name', '')
->assertValue('#signup_last_name', '')
->assertValue('#signup_login', '')
->assertValue('#signup_password', '')
->assertValue('#signup_password_confirmation', '');
// Submit invalid data
$step->type('#signup_login', '*')
->type('#signup_password', '12345678')
->type('#signup_password_confirmation', '123456789')
->click('[type=submit]')
->waitFor('#signup_login.is-invalid')
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password_input .invalid-feedback')
->assertFocused('#signup_login')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
// Submit valid data
$step->type('#signup_password_confirmation', '12345678')
->type('#signup_login', 'signuptestdusk')
->type('#signup_first_name', 'First')
->type('#signup_last_name', 'Last')
->click('[type=submit]');
})
// At this point we should be auto-logged-in to dashboard
->waitUntilMissing('@step3')
->waitUntilMissing('.app-loader')
->on(new Dashboard())
->assertUser('signuptestdusk@' . \config('app.domain'))
// Logout the user
->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
});
$invitation->refresh();
$user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first();
$this->assertTrue($invitation->isCompleted());
$this->assertSame($user->id, $invitation->user_id);
$this->assertSame('First', $user->getSetting('first_name'));
$this->assertSame('Last', $user->getSetting('last_name'));
$this->assertSame($invitation->email, $user->getSetting('external_email'));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Nov 21, 8:05 PM (4 h, 36 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
80039
Default Alt Text
(95 KB)

Event Timeline