-
-
-
+
+
+
{{ $t('signup.created') }}
+
+
+
+
+
{{ checkout.title }}
+
{{ checkout.content }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
index 9b4497e7..9c53634f 100644
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -1,861 +1,864 @@
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)))
->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('.card-text', 'The account is about to be created!')
- ->assertSeeIn('.card-text', 'You signed up for an account')
+ $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'));
}
}
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
index cd6e5de9..d4af6e38 100644
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -1,1080 +1,1098 @@
domain = $this->getPublicDomain();
$this->deleteTestUser("SignupControllerTest1@$this->domain");
$this->deleteTestUser("signuplogin@$this->domain");
$this->deleteTestUser("admin@external.com");
$this->deleteTestUser("test-inv@kolabnow.com");
$this->deleteTestDomain('external.com');
$this->deleteTestDomain('signup-domain.com');
$this->deleteTestGroup('group-test@kolabnow.com');
SI::truncate();
Plan::where('title', 'test')->delete();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestUser("SignupControllerTest1@$this->domain");
$this->deleteTestUser("signuplogin@$this->domain");
$this->deleteTestUser("admin@external.com");
$this->deleteTestUser("test-inv@kolabnow.com");
$this->deleteTestDomain('external.com');
$this->deleteTestDomain('signup-domain.com');
$this->deleteTestGroup('group-test@kolabnow.com');
SI::truncate();
Plan::where('title', 'test')->delete();
parent::tearDown();
}
/**
* Return a public domain for signup tests
*/
private function getPublicDomain(): string
{
if (!$this->domain) {
$this->refreshApplication();
$public_domains = Domain::getPublicDomains();
$this->domain = reset($public_domains);
if (empty($this->domain)) {
$this->domain = 'signup-domain.com';
Domain::create([
'namespace' => $this->domain,
'status' => Domain::STATUS_ACTIVE,
'type' => Domain::TYPE_PUBLIC,
]);
}
}
return $this->domain;
}
/**
* Test fetching public domains for signup
*/
public function testSignupDomains(): void
{
$response = $this->get('/api/auth/signup/domains');
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertSame(Domain::getPublicDomains(), $json['domains']);
}
/**
* Test fetching plans for signup
*/
public function testSignupPlans(): void
{
$individual = Plan::withEnvTenantContext()->where('title', 'individual')->first();
$group = Plan::withEnvTenantContext()->where('title', 'group')->first();
$response = $this->get('/api/auth/signup/plans');
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertCount(2, $json['plans']);
$this->assertSame($individual->title, $json['plans'][0]['title']);
$this->assertSame($individual->name, $json['plans'][0]['name']);
$this->assertSame($individual->description, $json['plans'][0]['description']);
$this->assertFalse($json['plans'][0]['isDomain']);
$this->assertArrayHasKey('button', $json['plans'][0]);
$this->assertSame($group->title, $json['plans'][1]['title']);
$this->assertSame($group->name, $json['plans'][1]['name']);
$this->assertSame($group->description, $json['plans'][1]['description']);
$this->assertTrue($json['plans'][1]['isDomain']);
$this->assertArrayHasKey('button', $json['plans'][1]);
}
/**
* Test fetching invitation
*/
public function testSignupInvitations(): void
{
Queue::fake();
$invitation = SI::create(['email' => 'email1@ext.com']);
// Test existing invitation
$response = $this->get("/api/auth/signup/invitations/{$invitation->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame($invitation->id, $json['id']);
// Test non-existing invitation
$response = $this->get("/api/auth/signup/invitations/abc");
$response->assertStatus(404);
// Test completed invitation
SI::where('id', $invitation->id)->update(['status' => SI::STATUS_COMPLETED]);
$response = $this->get("/api/auth/signup/invitations/{$invitation->id}");
$response->assertStatus(404);
}
/**
* Test signup initialization with invalid input
*/
public function testSignupInitInvalidInput(): void
{
// Empty input data
$data = [];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
// Data with missing name
$data = [
'email' => 'UsersApiControllerTest1@UsersApiControllerTest.com',
'first_name' => str_repeat('a', 250),
'last_name' => str_repeat('a', 250),
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('first_name', $json['errors']);
$this->assertArrayHasKey('last_name', $json['errors']);
// Data with invalid email (but not phone number)
$data = [
'email' => '@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
// Sanity check on voucher code, last/first name is optional
$data = [
'voucher' => '123456789012345678901234567890123',
'email' => 'valid@email.com',
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('voucher', $json['errors']);
// Email address too long
$data = [
'email' => str_repeat('a', 190) . '@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
SignupCode::truncate();
// Email address limit check
$data = [
'email' => 'test@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
];
\config(['app.signup.email_limit' => 0]);
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
\config(['app.signup.email_limit' => 1]);
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
// TODO: This probably should be a different message?
$this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
// IP address limit check
$data = [
'email' => 'ip@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
];
\config(['app.signup.email_limit' => 0]);
\config(['app.signup.ip_limit' => 0]);
$response = $this->post('/api/auth/signup/init', $data, ['REMOTE_ADDR' => '10.1.1.1']);
$json = $response->json();
$response->assertStatus(200);
\config(['app.signup.ip_limit' => 1]);
$response = $this->post('/api/auth/signup/init', $data, ['REMOTE_ADDR' => '10.1.1.1']);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
// TODO: This probably should be a different message?
$this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
// TODO: Test phone validation
}
/**
* Test signup initialization with valid input
*/
public function testSignupInitValidInput(): array
{
Queue::fake();
// Assert that no jobs were pushed...
Queue::assertNothingPushed();
$data = [
'email' => 'testuser@external.com',
'first_name' => 'Signup',
'last_name' => 'User',
'plan' => 'individual',
];
$response = $this->post('/api/auth/signup/init', $data, ['REMOTE_ADDR' => '10.1.1.2']);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
$code = SignupCode::find($json['code']);
$this->assertSame('10.1.1.2', $code->ip_address);
$this->assertSame(null, $code->verify_ip_address);
$this->assertSame(null, $code->submit_ip_address);
// Assert the email sending job was pushed once
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1);
// Assert the job has proper data assigned
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) {
$code = TestCase::getObjectProperty($job, 'code');
return $code->code === $json['code']
&& $code->plan === $data['plan']
&& $code->email === $data['email']
&& $code->first_name === $data['first_name']
&& $code->last_name === $data['last_name'];
});
// Try the same with voucher
$data['voucher'] = 'TEST';
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
// Assert the job has proper data assigned
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) {
$code = TestCase::getObjectProperty($job, 'code');
return $code->code === $json['code']
&& $code->plan === $data['plan']
&& $code->email === $data['email']
&& $code->voucher === $data['voucher']
&& $code->first_name === $data['first_name']
&& $code->last_name === $data['last_name'];
});
return [
'code' => $json['code'],
'email' => $data['email'],
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'plan' => $data['plan'],
'voucher' => $data['voucher']
];
}
/**
* Test signup code verification with invalid input
*
* @depends testSignupInitValidInput
*/
public function testSignupVerifyInvalidInput(array $result): void
{
// Empty data
$data = [];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('code', $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with existing code but missing short_code
$data = [
'code' => $result['code'],
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with invalid short_code
$data = [
'code' => $result['code'],
'short_code' => 'XXXX',
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// TODO: Test expired code
}
/**
* Test signup code verification with valid input
*
* @depends testSignupInitValidInput
*/
public function testSignupVerifyValidInput(array $result): array
{
$code = SignupCode::find($result['code']);
$code->ip_address = '10.1.1.2';
$code->save();
$data = [
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup/verify', $data, ['REMOTE_ADDR' => '10.1.1.3']);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(7, $json);
$this->assertSame('success', $json['status']);
$this->assertSame($result['email'], $json['email']);
$this->assertSame($result['first_name'], $json['first_name']);
$this->assertSame($result['last_name'], $json['last_name']);
$this->assertSame($result['voucher'], $json['voucher']);
$this->assertSame(false, $json['is_domain']);
$this->assertTrue(is_array($json['domains']) && !empty($json['domains']));
$code->refresh();
$this->assertSame('10.1.1.2', $code->ip_address);
$this->assertSame('10.1.1.3', $code->verify_ip_address);
$this->assertSame(null, $code->submit_ip_address);
return $result;
}
/**
* Test last signup step with invalid input
*
* @depends testSignupVerifyValidInput
*/
public function testSignupInvalidInput(array $result): void
{
// Empty data
$data = [];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(3, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
$this->assertArrayHasKey('domain', $json['errors']);
$domain = $this->getPublicDomain();
// Passwords do not match and missing domain
$data = [
'login' => 'test',
'password' => 'test',
'password_confirmation' => 'test2',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
$this->assertArrayHasKey('domain', $json['errors']);
$domain = $this->getPublicDomain();
// Login too short, password too short
$data = [
'login' => '1',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
// Missing codes
$data = [
'login' => 'login-valid',
'domain' => $domain,
'password' => 'testtest',
'password_confirmation' => 'testtest',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('code', $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with invalid short_code
$data = [
'login' => 'TestLogin',
'domain' => $domain,
'password' => 'testtest',
'password_confirmation' => 'testtest',
'code' => $result['code'],
'short_code' => 'XXXX',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
$code = SignupCode::find($result['code']);
// Data with invalid voucher
$data = [
'login' => 'TestLogin',
'domain' => $domain,
'password' => 'testtest',
'password_confirmation' => 'testtest',
'code' => $result['code'],
'short_code' => $code->short_code,
'voucher' => 'XXX',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('voucher', $json['errors']);
// Valid code, invalid login
$data = [
'login' => 'żżżżżż',
'domain' => $domain,
'password' => 'testtest',
'password_confirmation' => 'testtest',
'code' => $result['code'],
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
}
/**
* Test last signup step with valid input (user creation)
*
* @depends testSignupVerifyValidInput
*/
public function testSignupValidInput(array $result): void
{
$queue = Queue::fake();
$domain = $this->getPublicDomain();
$identity = \strtolower('SignupLogin@') . $domain;
$code = SignupCode::find($result['code']);
$code->ip_address = '10.1.1.2';
$code->verify_ip_address = '10.1.1.3';
$code->save();
$data = [
'login' => 'SignupLogin',
'domain' => $domain,
'password' => 'testtest',
'password_confirmation' => 'testtest',
'code' => $code->code,
'short_code' => $code->short_code,
'voucher' => 'TEST',
];
$response = $this->post('/api/auth/signup', $data, ['REMOTE_ADDR' => '10.1.1.4']);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame('bearer', $json['token_type']);
$this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0);
$this->assertNotEmpty($json['access_token']);
$this->assertSame($identity, $json['email']);
Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1);
Queue::assertPushed(
\App\Jobs\User\CreateJob::class,
function ($job) use ($data) {
$userEmail = TestCase::getObjectProperty($job, 'userEmail');
return $userEmail === \strtolower($data['login'] . '@' . $data['domain']);
}
);
$code->refresh();
// Check if the user has been created
$user = User::where('email', $identity)->first();
$this->assertNotEmpty($user);
$this->assertSame($identity, $user->email);
$this->assertTrue($user->isRestricted());
// Check if the code has been updated and soft-deleted
$this->assertTrue($code->trashed());
$this->assertSame('10.1.1.2', $code->ip_address);
$this->assertSame('10.1.1.3', $code->verify_ip_address);
$this->assertSame('10.1.1.4', $code->submit_ip_address);
$this->assertSame($user->id, $code->user_id);
// Check user settings
$this->assertSame($result['first_name'], $user->getSetting('first_name'));
$this->assertSame($result['last_name'], $user->getSetting('last_name'));
$this->assertSame($result['email'], $user->getSetting('external_email'));
// Discount
$discount = Discount::where('code', 'TEST')->first();
$this->assertSame($discount->id, $user->wallets()->first()->discount_id);
// TODO: Check SKUs/Plan
// TODO: Check if the access token works
}
/**
* Test signup for a group (custom domain) account
*/
public function testSignupGroupAccount(): void
{
Queue::fake();
// Initial signup request
$user_data = $data = [
'email' => 'testuser@external.com',
'first_name' => 'Signup',
'last_name' => 'User',
'plan' => 'group',
];
$response = $this->withoutMiddleware()->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
// Assert the email sending job was pushed once
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1);
// Assert the job has proper data assigned
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) {
$code = TestCase::getObjectProperty($job, 'code');
return $code->code === $json['code']
&& $code->plan === $data['plan']
&& $code->email === $data['email']
&& $code->first_name === $data['first_name']
&& $code->last_name === $data['last_name'];
});
// Verify the code
$code = SignupCode::find($json['code']);
$data = [
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup/verify', $data);
$result = $response->json();
$response->assertStatus(200);
$this->assertCount(7, $result);
$this->assertSame('success', $result['status']);
$this->assertSame($user_data['email'], $result['email']);
$this->assertSame($user_data['first_name'], $result['first_name']);
$this->assertSame($user_data['last_name'], $result['last_name']);
$this->assertSame(null, $result['voucher']);
$this->assertSame(true, $result['is_domain']);
$this->assertSame([], $result['domains']);
// Final signup request
$login = 'admin';
$domain = 'external.com';
$data = [
'login' => $login,
'domain' => $domain,
'password' => 'testtest',
'password_confirmation' => 'testtest',
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup', $data);
$result = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $result['status']);
$this->assertSame('bearer', $result['token_type']);
$this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0);
$this->assertNotEmpty($result['access_token']);
$this->assertSame("$login@$domain", $result['email']);
Queue::assertPushed(\App\Jobs\Domain\CreateJob::class, 1);
Queue::assertPushed(
\App\Jobs\Domain\CreateJob::class,
function ($job) use ($domain) {
$domainNamespace = TestCase::getObjectProperty($job, 'domainNamespace');
return $domainNamespace === $domain;
}
);
Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1);
Queue::assertPushed(
\App\Jobs\User\CreateJob::class,
function ($job) use ($data) {
$userEmail = TestCase::getObjectProperty($job, 'userEmail');
return $userEmail === $data['login'] . '@' . $data['domain'];
}
);
// Check if the code has been removed
$code->refresh();
$this->assertTrue($code->trashed());
// Check if the user has been created
$user = User::where('email', $login . '@' . $domain)->first();
$this->assertNotEmpty($user);
$this->assertTrue($user->isRestricted());
// Check user settings
$this->assertSame($user_data['email'], $user->getSetting('external_email'));
$this->assertSame($user_data['first_name'], $user->getSetting('first_name'));
$this->assertSame($user_data['last_name'], $user->getSetting('last_name'));
// TODO: Check domain record
// TODO: Check SKUs/Plan
// TODO: Check if the access token works
}
/**
* Test signup with mode=mandate
+ *
+ * @group mollie
*/
public function testSignupMandateMode(): void
{
Queue::fake();
+ \config(['services.payment_provider' => 'mollie']);
+
$plan = Plan::create([
'title' => 'test',
'name' => 'Test Account',
'description' => 'Test',
'free_months' => 1,
'discount_qty' => 0,
'discount_rate' => 0,
'mode' => Plan::MODE_MANDATE,
]);
$packages = [
Package::where(['title' => 'kolab', 'tenant_id' => \config('app.tenant_id')])->first()
];
$plan->packages()->saveMany($packages);
$post = [
'plan' => 'abc',
'login' => 'test-inv',
'domain' => 'kolabnow.com',
'password' => 'testtest',
'password_confirmation' => 'testtest',
];
// Test invalid plan identifier
$response = $this->post('/api/auth/signup', $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertSame("The selected plan is invalid.", $json['errors']['plan']);
// Test valid input
$post['plan'] = $plan->title;
$response = $this->post('/api/auth/signup', $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertNotEmpty($json['access_token']);
$this->assertSame('test-inv@kolabnow.com', $json['email']);
$this->assertTrue($json['isLocked']);
$user = User::where('email', 'test-inv@kolabnow.com')->first();
$this->assertNotEmpty($user);
$this->assertSame($plan->id, $user->getSetting('plan_id'));
+ $this->assertSame('You are choosing a monthly subscription.', $json['checkout']['title']);
+ $this->assertTrue(!empty($json['checkout']['id']));
}
/**
* Test signup via invitation
*/
public function testSignupInvitation(): void
{
Queue::fake();
$invitation = SI::create(['email' => 'email1@ext.com']);
$post = [
'invitation' => 'abc',
'first_name' => 'Signup',
'last_name' => 'User',
'login' => 'test-inv',
'domain' => 'kolabnow.com',
'password' => 'testtest',
'password_confirmation' => 'testtest',
];
// Test invalid invitation identifier
$response = $this->post('/api/auth/signup', $post);
$response->assertStatus(404);
// Test valid input
$post['invitation'] = $invitation->id;
$response = $this->post('/api/auth/signup', $post);
$result = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $result['status']);
$this->assertSame('bearer', $result['token_type']);
$this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0);
$this->assertNotEmpty($result['access_token']);
$this->assertSame('test-inv@kolabnow.com', $result['email']);
// Check if the user has been created
$user = User::where('email', 'test-inv@kolabnow.com')->first();
$this->assertNotEmpty($user);
// Check user settings
$this->assertSame($invitation->email, $user->getSetting('external_email'));
$this->assertSame($post['first_name'], $user->getSetting('first_name'));
$this->assertSame($post['last_name'], $user->getSetting('last_name'));
$invitation->refresh();
$this->assertSame($user->id, $invitation->user_id);
$this->assertTrue($invitation->isCompleted());
// TODO: Test POST params validation
}
/**
* Test signup validation (POST /signup/validate)
*/
public function testSignupValidate(): void
{
Queue::fake();
$plan = Plan::create([
'title' => 'test',
'name' => 'Test Account',
'description' => 'Test',
'free_months' => 1,
'months' => 12,
'discount_qty' => 0,
'discount_rate' => 0,
'mode' => Plan::MODE_MANDATE,
]);
$packages = [
Package::where(['title' => 'kolab', 'tenant_id' => \config('app.tenant_id')])->first()
];
$plan->packages()->saveMany($packages);
$post = [
'login' => 'i',
'password' => 'testtest',
'password_confirmation' => 'testtest1',
'voucher' => str_repeat('a', 33),
];
// Test basic input validation
$response = $this->post('/api/auth/signup/validate', $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(4, $json['errors']);
$this->assertSame(["The login must be at least 2 characters."], $json['errors']['login']);
$this->assertSame(["The password confirmation does not match."], $json['errors']['password']);
$this->assertSame(["The domain field is required."], $json['errors']['domain']);
$this->assertSame(["The voucher may not be greater than 32 characters."], $json['errors']['voucher']);
// Test with mode=mandate plan, but invalid voucher code
$post = [
'login' => 'test-inv',
'domain' => 'kolabnow.com',
'password' => 'testtest',
'password_confirmation' => 'testtest',
'plan' => $plan->title,
'voucher' => 'non-existing',
];
$response = $this->post('/api/auth/signup/validate', $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertSame("The voucher code is invalid or expired.", $json['errors']['voucher']);
// Test with mode=mandate plan, and valid voucher code
$post['voucher'] = 'TEST';
$response = $this->post('/api/auth/signup/validate', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('success', $json['status']);
- $this->assertStringContainsString('106,92 CHF', $json['content']);
+ $this->assertTrue(empty($json['id']));
+ $this->assertTrue(!empty($json['content']));
+ $this->assertSame('You are choosing a yearly subscription.', $json['title']);
+ $this->assertSame(
+ '
'
+ . 'Yearly subscription | 118,80 CHF |
'
+ . 'Discount: TEST | -11,88 CHF |
'
+ . ' |
'
+ . 'Total | 106,92 CHF |
'
+ . '*Incl. VAT 8,23 CHF (7,7 % of 98,69 CHF) |
'
+ . '
',
+ $json['summary']
+ );
// TODO: Test other plan modes
}
/**
* List of login/domain validation cases for testValidateLogin()
*
* @return array Arguments for testValidateLogin()
*/
public function dataValidateLogin(): array
{
$domain = $this->getPublicDomain();
return [
// Individual account
['', $domain, false, ['login' => 'The login field is required.']],
['test123456', 'localhost', false, ['domain' => 'The specified domain is invalid.']],
['test123456', 'unknown-domain.org', false, ['domain' => 'The specified domain is invalid.']],
['test.test', $domain, false, null],
['test_test', $domain, false, null],
['test-test', $domain, false, null],
['admin', $domain, false, ['login' => 'The specified login is not available.']],
['administrator', $domain, false, ['login' => 'The specified login is not available.']],
['sales', $domain, false, ['login' => 'The specified login is not available.']],
['root', $domain, false, ['login' => 'The specified login is not available.']],
// Domain account
['admin', 'kolabsys.com', true, null],
['testnonsystemdomain', 'invalid', true, ['domain' => 'The specified domain is invalid.']],
['testnonsystemdomain', '.com', true, ['domain' => 'The specified domain is invalid.']],
];
}
/**
* Signup login/domain validation.
*
* Note: Technically these include unit tests, but let's keep it here for now.
* FIXME: Shall we do a http request for each case?
*
* @dataProvider dataValidateLogin
*/
public function testValidateLogin($login, $domain, $external, $expected_result): void
{
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
$this->assertSame($expected_result, $result);
}
/**
* Signup login/domain validation, more cases
*/
public function testValidateLoginMore(): void
{
Queue::fake();
// Test registering for an email of an existing group
$login = 'group-test';
$domain = 'kolabnow.com';
$group = $this->getTestGroup("{$login}@{$domain}");
$external = false;
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
$this->assertSame(['login' => 'The specified login is not available.'], $result);
// Test registering for an email of an existing, but soft-deleted group
$group->delete();
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
$this->assertSame(['login' => 'The specified login is not available.'], $result);
// Test registering for an email of an existing user
$domain = $this->getPublicDomain();
$login = 'signuplogin';
$user = $this->getTestUser("{$login}@{$domain}");
$external = false;
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
$this->assertSame(['login' => 'The specified login is not available.'], $result);
// Test registering for an email of an existing, but soft-deleted user
$user->delete();
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
$this->assertSame(['login' => 'The specified login is not available.'], $result);
// Test registering for a domain that exists
$external = true;
$domain = $this->getTestDomain(
'external.com',
['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL]
);
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain->namespace, $external]);
$this->assertSame(['domain' => 'The specified domain is not available.'], $result);
// Test registering for a domain that exists but is soft-deleted
$domain->delete();
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain->namespace, $external]);
$this->assertSame(['domain' => 'The specified domain is not available.'], $result);
}
}
diff --git a/src/tests/Unit/UtilsTest.php b/src/tests/Unit/UtilsTest.php
index e4debe70..c6838e95 100644
--- a/src/tests/Unit/UtilsTest.php
+++ b/src/tests/Unit/UtilsTest.php
@@ -1,206 +1,216 @@
delete();
\App\IP6Net::where('net_number', inet_pton('2001:db8::ff00:42:0'))->delete();
$this->assertSame('', Utils::countryForIP('127.0.0.1', ''));
$this->assertSame('CH', Utils::countryForIP('127.0.0.1'));
$this->assertSame('', Utils::countryForIP('2001:db8::ff00:42:1', ''));
$this->assertSame('CH', Utils::countryForIP('2001:db8::ff00:42:1'));
\App\IP4Net::create([
'net_number' => '127.0.0.0',
'net_broadcast' => '127.255.255.255',
'net_mask' => 8,
'country' => 'US',
'rir_name' => 'test',
'serial' => 1,
]);
\App\IP6Net::create([
'net_number' => '2001:db8::ff00:42:0',
'net_broadcast' => \App\Utils::ip6Broadcast('2001:db8::ff00:42:0', 8),
'net_mask' => 8,
'country' => 'PL',
'rir_name' => 'test',
'serial' => 1,
]);
$this->assertSame('US', Utils::countryForIP('127.0.0.1', ''));
$this->assertSame('US', Utils::countryForIP('127.0.0.1'));
$this->assertSame('PL', Utils::countryForIP('2001:db8::ff00:42:1', ''));
$this->assertSame('PL', Utils::countryForIP('2001:db8::ff00:42:1'));
\App\IP4Net::where('net_number', inet_pton('127.0.0.0'))->delete();
\App\IP6Net::where('net_number', inet_pton('2001:db8::ff00:42:0'))->delete();
}
/**
* Test for Utils::emailToLower()
*/
public function testEmailToLower(): void
{
$this->assertSame('test@test.tld', Utils::emailToLower('test@Test.Tld'));
$this->assertSame('test@test.tld', Utils::emailToLower('Test@Test.Tld'));
$this->assertSame('shared+shared/Test@test.tld', Utils::emailToLower('shared+shared/Test@Test.Tld'));
}
/**
* Test for Utils::money()
*/
public function testMoney(): void
{
$this->assertSame('-0,01 CHF', Utils::money(-1, 'CHF'));
$this->assertSame('0,00 CHF', Utils::money(0, 'CHF'));
$this->assertSame('1,11 €', Utils::money(111, 'EUR'));
$this->assertSame('1,00 CHF', Utils::money(100, 'CHF'));
$this->assertSame('€0.00', Utils::money(0, 'EUR', 'en_US'));
}
+ /**
+ * Test for Utils::percent()
+ */
+ public function testPercent(): void
+ {
+ $this->assertSame('0 %', Utils::percent(0));
+ $this->assertSame('10 %', Utils::percent(10.0));
+ $this->assertSame('10,12 %', Utils::percent(10.12));
+ }
+
/**
* Test for Utils::normalizeAddress()
*/
public function testNormalizeAddress(): void
{
$this->assertSame('', Utils::normalizeAddress(''));
$this->assertSame('', Utils::normalizeAddress(null));
$this->assertSame('test', Utils::normalizeAddress('TEST'));
$this->assertSame('test@domain.tld', Utils::normalizeAddress('Test@Domain.TLD'));
$this->assertSame('test@domain.tld', Utils::normalizeAddress('Test+Trash@Domain.TLD'));
$this->assertSame(['', ''], Utils::normalizeAddress('', true));
$this->assertSame(['', ''], Utils::normalizeAddress(null, true));
$this->assertSame(['test', ''], Utils::normalizeAddress('TEST', true));
$this->assertSame(['test', 'domain.tld'], Utils::normalizeAddress('Test@Domain.TLD', true));
$this->assertSame(['test', 'domain.tld'], Utils::normalizeAddress('Test+Trash@Domain.TLD', true));
}
/**
* Test for Utils::powerSet()
*/
public function testPowerSet(): void
{
$set = [];
$result = Utils::powerSet($set);
$this->assertIsArray($result);
$this->assertCount(0, $result);
$set = ["a1"];
$result = Utils::powerSet($set);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$this->assertTrue(in_array(["a1"], $result));
$set = ["a1", "a2"];
$result = Utils::powerSet($set);
$this->assertIsArray($result);
$this->assertCount(3, $result);
$this->assertTrue(in_array(["a1"], $result));
$this->assertTrue(in_array(["a2"], $result));
$this->assertTrue(in_array(["a1", "a2"], $result));
$set = ["a1", "a2", "a3"];
$result = Utils::powerSet($set);
$this->assertIsArray($result);
$this->assertCount(7, $result);
$this->assertTrue(in_array(["a1"], $result));
$this->assertTrue(in_array(["a2"], $result));
$this->assertTrue(in_array(["a3"], $result));
$this->assertTrue(in_array(["a1", "a2"], $result));
$this->assertTrue(in_array(["a1", "a3"], $result));
$this->assertTrue(in_array(["a2", "a3"], $result));
$this->assertTrue(in_array(["a1", "a2", "a3"], $result));
}
/**
* Test for Utils::serviceUrl()
*/
public function testServiceUrl(): void
{
$public_href = 'https://public.url/cockpit';
$local_href = 'https://local.url/cockpit';
\config([
'app.url' => $local_href,
'app.public_url' => '',
]);
$this->assertSame($local_href, Utils::serviceUrl(''));
$this->assertSame($local_href . '/unknown', Utils::serviceUrl('unknown'));
$this->assertSame($local_href . '/unknown', Utils::serviceUrl('/unknown'));
\config([
'app.url' => $local_href,
'app.public_url' => $public_href,
]);
$this->assertSame($public_href, Utils::serviceUrl(''));
$this->assertSame($public_href . '/unknown', Utils::serviceUrl('unknown'));
$this->assertSame($public_href . '/unknown', Utils::serviceUrl('/unknown'));
}
/**
* Test for Utils::uuidInt()
*/
public function testUuidInt(): void
{
$result = Utils::uuidInt();
$this->assertTrue(is_int($result));
$this->assertTrue($result > 0);
}
/**
* Test for Utils::uuidStr()
*/
public function testUuidStr(): void
{
$result = Utils::uuidStr();
$this->assertTrue(is_string($result));
$this->assertTrue(strlen($result) === 36);
$this->assertTrue(preg_match('/[^a-f0-9-]/i', $result) === 0);
}
/**
* Test for Utils::exchangeRate()
*/
public function testExchangeRate(): void
{
$this->assertSame(1.0, Utils::exchangeRate("DUMMY", "dummy"));
// Exchange rates are volatile, can't test with high accuracy.
$this->assertTrue(Utils::exchangeRate("CHF", "EUR") >= 0.88);
//$this->assertEqualsWithDelta(0.90503424978382, Utils::exchangeRate("CHF", "EUR"), PHP_FLOAT_EPSILON);
$this->assertTrue(Utils::exchangeRate("EUR", "CHF") <= 1.12);
//$this->assertEqualsWithDelta(1.1049305595217682, Utils::exchangeRate("EUR", "CHF"), PHP_FLOAT_EPSILON);
$this->expectException(\Exception::class);
$this->assertSame(1.0, Utils::exchangeRate("CHF", "FOO"));
$this->expectException(\Exception::class);
$this->assertSame(1.0, Utils::exchangeRate("FOO", "CHF"));
}
}