Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F86182
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
95 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment