-
+
+
+
Sign Up - Step 1/3
Sign up to start your free month.
Sign Up - Step 2/3
We sent out a confirmation code to your email address.
Enter the code we sent you, or click the link in the message.
Sign Up - Step 3/3
Create your Kolab identity (you can choose additional addresses later).
diff --git a/src/routes/api.php b/src/routes/api.php
index c4906bb9..d2d89493 100644
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -1,47 +1,48 @@
'api',
'prefix' => 'auth'
],
function ($router) {
Route::get('info', 'API\UsersController@info');
Route::post('login', 'API\UsersController@login');
Route::post('logout', 'API\UsersController@logout');
Route::post('refresh', 'API\UsersController@refresh');
Route::post('password-reset/init', 'API\PasswordResetController@init');
Route::post('password-reset/verify', 'API\PasswordResetController@verify');
Route::post('password-reset', 'API\PasswordResetController@reset');
+ Route::get('signup/plans', 'API\SignupController@plans');
Route::post('signup/init', 'API\SignupController@init');
Route::post('signup/verify', 'API\SignupController@verify');
Route::post('signup', 'API\SignupController@signup');
}
);
Route::group(
[
'middleware' => 'auth:api',
'prefix' => 'v4'
],
function () {
Route::apiResource('entitlements', API\EntitlementsController::class);
Route::apiResource('users', API\UsersController::class);
Route::apiResource('wallets', API\WalletsController::class);
}
);
diff --git a/src/tests/Browser/Pages/Signup.php b/src/tests/Browser/Pages/Signup.php
index de74b560..22f6e244 100644
--- a/src/tests/Browser/Pages/Signup.php
+++ b/src/tests/Browser/Pages/Signup.php
@@ -1,47 +1,51 @@
assertPathIs('/signup');
- $browser->assertPresent('@step1');
+ $browser->assertPathIs('/signup')
+ ->assertPresent('@step0')
+ ->assertPresent('@step1')
+ ->assertPresent('@step2')
+ ->assertPresent('@step3');
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements(): array
{
return [
'@app' => '#app',
+ '@step0' => '#step0',
'@step1' => '#step1',
'@step2' => '#step2',
'@step3' => '#step3',
];
}
}
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
index 025a1de2..72813daf 100644
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -1,324 +1,494 @@
delete();
+ parent::setUp();
+
+ Domain::where('namespace', 'user-domain-signup.com')->delete();
+ User::where('email', 'signuptestdusk@' . \config('app.domain'))
+ ->orWhere('email', 'admin@user-domain-signup.com')
+ ->delete();
}
/**
* Test signup code verification with a link
- *
- * @return void
*/
- public function testSignupCodeByLink()
+ 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');
$browser->waitFor('.toast-error');
$browser->click('.toast-error'); // remove the toast
});
// Test valid code
$this->browse(function (Browser $browser) {
$code = SignupCode::create([
'data' => [
'email' => 'User@example.org',
'name' => 'User Name',
'plan' => 'individual',
]
]);
$browser->visit('/signup/' . $code->short_code . '-' . $code->code);
$browser->waitFor('@step3');
$browser->assertMissing('@step1');
$browser->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 1st step of the signup process
- *
- * @return void
+ * Test signup "welcome" page
*/
- public function testSignupStep1()
+ 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(['signup', 'explore', 'blog', 'support', 'login']);
+ $browser->assertActiveItem('signup');
+ });
+
+ $browser->waitFor('@step0 .plan-selector > .plan-box');
+
+ // Assert first plan box and press the button
+ $browser->with('@step0 .plan-selector > .plan-individual', function ($step) {
+ $step->assertVisible('button')
+ ->assertSeeIn('button', 'individual')
+ ->assertVisible('.plan-description')
+ ->click('button');
+ });
+
+ $browser->waitForLocation('/signup/individual')
+ ->assertVisible('@step1')
+ ->assertMissing('@step0')
+ ->assertMissing('@step2')
+ ->assertMissing('@step3')
+ ->assertFocused('@step1 #signup_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_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());
+
$browser->assertVisible('@step1');
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
$browser->assertActiveItem('signup');
});
- // Here we expect two text inputs and Continue
+ // Here we expect two text inputs and Back and Continue buttons
$browser->with('@step1', function ($step) {
- $step->assertVisible('#signup_name');
- $step->assertFocused('#signup_name');
- $step->assertVisible('#signup_email');
- $step->assertVisible('[type=submit]');
+ $step->assertVisible('#signup_name')
+ ->assertFocused('#signup_name')
+ ->assertVisible('#signup_email')
+ ->assertVisible('[type=button]')
+ ->assertVisible('[type=submit]');
});
// Submit empty form
// Both Step 1 inputs are required, so after pressing Submit
// we expect focus to be moved to the first input
$browser->with('@step1', function ($step) {
$step->click('[type=submit]');
$step->assertFocused('#signup_name');
});
// Submit invalid email
// We expect email input to have is-invalid class added, with .invalid-feedback element
$browser->with('@step1', function ($step) use ($browser) {
$step->type('#signup_name', 'Test User');
$step->type('#signup_email', '@test');
$step->click('[type=submit]');
$step->waitFor('#signup_email.is-invalid');
$step->waitFor('#signup_email + .invalid-feedback');
$browser->waitFor('.toast-error');
$browser->click('.toast-error'); // remove the toast
});
// 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_name', 'Test User');
$step->type('#signup_email', 'BrowserSignupTestUser1@kolab.org');
$step->click('[type=submit]');
$step->assertMissing('#signup_email.is-invalid');
$step->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
- * @return void
*/
- public function testSignupStep2()
+ public function testSignupStep2(): void
{
$this->browse(function (Browser $browser) {
- $browser->assertVisible('@step2');
+ $browser->assertVisible('@step2')
+ ->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');
- $step->assertFocused('#signup_short_code');
- $step->assertVisible('[type=button]');
- $step->assertVisible('[type=submit]');
+ $step->assertVisible('#signup_short_code')
+ ->assertFocused('#signup_short_code')
+ ->assertVisible('[type=button]')
+ ->assertVisible('[type=submit]');
});
// Test Back button functionality
- $browser->click('@step2 [type=button]');
- $browser->waitFor('@step1');
- $browser->assertFocused('@step1 #signup_name');
- $browser->assertMissing('@step2');
+ $browser->click('@step2 [type=button]')
+ ->waitFor('@step1')
+ ->assertFocused('@step1 #signup_name')
+ ->assertMissing('@step2');
// Submit valid Step 1 data (again)
$browser->with('@step1', function ($step) {
$step->type('#signup_name', 'Test User');
$step->type('#signup_email', 'BrowserSignupTestUser1@kolab.org');
$step->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) use ($browser) {
$step->type('#signup_short_code', 'XXXXX');
$step->click('[type=submit]');
$browser->waitFor('.toast-error');
$step->assertVisible('#signup_short_code.is-invalid');
$step->assertVisible('#signup_short_code + .invalid-feedback');
$step->assertFocused('#signup_short_code');
$browser->click('.toast-error'); // remove the toast
});
// 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
- * @return void
*/
- public function testSignupStep3()
+ 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) {
$step->assertVisible('#signup_login');
$step->assertVisible('#signup_password');
$step->assertVisible('#signup_confirm');
$step->assertVisible('select#signup_domain');
$step->assertVisible('[type=button]');
$step->assertVisible('[type=submit]');
$step->assertFocused('#signup_login');
$step->assertValue('select#signup_domain', \config('app.domain'));
$step->assertValue('#signup_login', '');
$step->assertValue('#signup_password', '');
$step->assertValue('#signup_confirm', '');
// 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) use ($browser) {
$step->assertFocused('#signup_login');
$step->type('#signup_login', '*');
$step->type('#signup_password', '12345678');
$step->type('#signup_confirm', '123456789');
$step->click('[type=submit]');
$browser->waitFor('.toast-error');
$step->assertVisible('#signup_login.is-invalid');
$step->assertVisible('#signup_domain + .invalid-feedback');
$step->assertVisible('#signup_password.is-invalid');
$step->assertVisible('#signup_password + .invalid-feedback');
$step->assertFocused('#signup_login');
$browser->click('.toast-error'); // remove the toast
});
// Submit invalid data (valid login, invalid password)
$browser->with('@step3', function ($step) use ($browser) {
$step->type('#signup_login', 'SignupTestDusk');
$step->click('[type=submit]');
$browser->waitFor('.toast-error');
$step->assertVisible('#signup_password.is-invalid');
$step->assertVisible('#signup_password + .invalid-feedback');
$step->assertMissing('#signup_login.is-invalid');
$step->assertMissing('#signup_domain + .invalid-feedback');
$step->assertFocused('#signup_password');
$browser->click('.toast-error'); // remove the toast
});
// Submit valid data
$browser->with('@step3', function ($step) {
$step->type('#signup_confirm', '12345678');
$step->click('[type=submit]');
});
$browser->waitUntilMissing('@step3');
// At this point we should be auto-logged-in to dashboard
$dashboard = new Dashboard();
$dashboard->assert($browser);
// FIXME: Is it enough to be sure user is logged in?
+
+ // Logout the user
+ // TODO: Test what happens if you goto /signup with active session
+ $browser->click('a.link-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->click('@step0 .plan-selector > .plan-group button')
+ ->waitForLocation('/signup/group');
+
+ $browser->assertVisible('@step1');
+
+ // 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_name', 'Test User')
+ ->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
+ ->click('[type=submit]');
+ });
+
+ $browser->waitFor('@step2');
+
+ // Submit valid code
+ $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');
+ $code = SignupCode::find($code);
+
+ $step->type('#signup_short_code', $code->short_code)
+ ->click('[type=submit]');
+ });
+
+ $browser->waitFor('@step3');
+
+ // Here we expect 4 text inputs, Back and Continue buttons
+ $browser->with('@step3', function ($step) {
+ $step->assertVisible('#signup_login')
+ ->assertVisible('#signup_password')
+ ->assertVisible('#signup_confirm')
+ ->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_confirm', '');
+ });
+
+ // Submit invalid login and password data
+ $browser->with('@step3', function ($step) use ($browser) {
+ $step->assertFocused('#signup_login')
+ ->type('#signup_login', '*')
+ ->type('#signup_domain', 'test.com')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_confirm', '123456789')
+ ->click('[type=submit]');
+
+ $browser->waitFor('.toast-error');
+
+ $step->assertVisible('#signup_login.is-invalid')
+ ->assertVisible('#signup_domain + .invalid-feedback')
+ ->assertVisible('#signup_password.is-invalid')
+ ->assertVisible('#signup_password + .invalid-feedback')
+ ->assertFocused('#signup_login');
+
+ $browser->click('.toast-error'); // remove the toast
+ });
+
+ // Submit invalid domain
+ $browser->with('@step3', function ($step) use ($browser) {
+ $step->type('#signup_login', 'admin')
+ ->type('#signup_domain', 'aaa')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_confirm', '12345678')
+ ->click('[type=submit]');
+
+ $browser->waitFor('.toast-error');
+
+ $step->assertMissing('#signup_login.is-invalid')
+ ->assertVisible('#signup_domain.is-invalid + .invalid-feedback')
+ ->assertMissing('#signup_password.is-invalid')
+ ->assertMissing('#signup_password + .invalid-feedback')
+ ->assertFocused('#signup_domain');
+
+ $browser->click('.toast-error'); // remove the toast
+ });
+
+ // Submit invalid domain
+ $browser->with('@step3', function ($step) use ($browser) {
+ $step->type('#signup_domain', 'user-domain-signup.com')
+ ->click('[type=submit]');
+ });
+
+ $browser->waitUntilMissing('@step3');
+
+ // At this point we should be auto-logged-in to dashboard
+ $dashboard = new Dashboard();
+ $dashboard->assert($browser);
+
+ // FIXME: Is it enough to be sure user is logged in?
+ $browser->click('a.link-logout');
});
}
}
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
index 5d58c182..e3f72c8d 100644
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -1,604 +1,622 @@
"SignupControllerTest1@$domain"]);
}
/**
* {@inheritDoc}
*
* @return void
*/
public function tearDown(): void
{
$domain = self::getPublicDomain();
User::where('email', "signuplogin@$domain")
->orWhere('email', "SignupControllerTest1@$domain")
->orWhere('email', 'admin@external.com')
->delete();
Domain::where('namespace', 'signup-domain.com')
->orWhere('namespace', 'external.com')
->delete();
}
/**
* Return a public domain for signup tests
*/
public function getPublicDomain(): string
{
if (!self::$domain) {
$this->refreshApplication();
self::$domain = Domain::getPublicDomains()[0];
if (empty(self::$domain)) {
self::$domain = 'signup-domain.com';
Domain::create([
'namespace' => self::$domain,
'status' => Domain::STATUS_Active,
'type' => Domain::TYPE_PUBLIC,
]);
}
}
return self::$domain;
}
+ /**
+ * Test fetching plans for signup
+ *
+ * @return void
+ */
+ public function testSignupPlans()
+ {
+ // Note: this uses plans that already have been seeded into the DB
+
+ $response = $this->get('/api/auth/signup/plans');
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $this->assertSame('success', $json['status']);
+ $this->assertCount(2, $json['plans']);
+ }
+
/**
* Test signup initialization with invalid input
*
* @return void
*/
public function testSignupInitInvalidInput()
{
// 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(2, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
$this->assertArrayHasKey('name', $json['errors']);
// Data with missing name
$data = [
'email' => 'UsersApiControllerTest1@UsersApiControllerTest.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('name', $json['errors']);
// Data with invalid email (but not phone number)
$data = [
'email' => '@example.org',
'name' => 'Signup 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']);
// TODO: Test phone validation
}
/**
* Test signup initialization with valid input
*
* @return array
*/
public function testSignupInitValidInput()
{
Queue::fake();
// Assert that no jobs were pushed...
Queue::assertNothingPushed();
$data = [
'email' => 'testuser@external.com',
'name' => 'Signup User',
'plan' => 'individual',
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$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) {
// Access protected property
$reflection = new \ReflectionClass($job);
$code = $reflection->getProperty('code');
$code->setAccessible(true);
$code = $code->getValue($job);
return $code->code === $json['code']
&& $code->data['plan'] === $data['plan']
&& $code->data['email'] === $data['email']
&& $code->data['name'] === $data['name'];
});
return [
'code' => $json['code'],
'email' => $data['email'],
'name' => $data['name'],
'plan' => $data['plan'],
];
}
/**
* Test signup code verification with invalid input
*
* @depends testSignupInitValidInput
* @return void
*/
public function testSignupVerifyInvalidInput(array $result)
{
// 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
*
* @return array
*/
public function testSignupVerifyValidInput(array $result)
{
$code = SignupCode::find($result['code']);
$data = [
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(5, $json);
$this->assertSame('success', $json['status']);
$this->assertSame($result['email'], $json['email']);
$this->assertSame($result['name'], $json['name']);
$this->assertSame(false, $json['is_domain']);
$this->assertTrue(is_array($json['domains']) && !empty($json['domains']));
return $result;
}
/**
* Test last signup step with invalid input
*
* @depends testSignupVerifyValidInput
* @return void
*/
public function testSignupInvalidInput(array $result)
{
// 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
$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(1, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
// Missing codes
$data = [
'login' => 'login-valid',
'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('code', $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with invalid short_code
$data = [
'login' => 'TestLogin',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'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']);
// Valid code, invalid login
$code = SignupCode::find($result['code']);
$data = [
'login' => 'żżżżżż',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'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
* @return void
*/
public function testSignupValidInput(array $result)
{
$domain = $this->getPublicDomain();
$identity = \strtolower('SignupLogin@') . $domain;
$code = SignupCode::find($result['code']);
$data = [
'login' => 'SignupLogin',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(4, $json);
$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']);
// Check if the code has been removed
$this->assertNull(SignupCode::where($result['code'])->first());
// Check if the user has been created
$user = User::where('email', $identity)->first();
$this->assertNotEmpty($user);
$this->assertSame($identity, $user->email);
$this->assertSame($result['name'], $user->name);
// Check external email in user settings
$this->assertSame($result['email'], $user->getSetting('external_email'));
// TODO: Check SKUs/Plan
// TODO: Check if the access token works
}
/**
* Test signup for a group (custom domain) account
*
* @return void
*/
public function testSignupGroupAccount()
{
Queue::fake();
// Initial signup request
$user_data = $data = [
'email' => 'testuser@external.com',
'name' => 'Signup User',
'plan' => 'group',
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$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) {
// Access protected property
$reflection = new \ReflectionClass($job);
$code = $reflection->getProperty('code');
$code->setAccessible(true);
$code = $code->getValue($job);
return $code->code === $json['code']
&& $code->data['plan'] === $data['plan']
&& $code->data['email'] === $data['email']
&& $code->data['name'] === $data['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(5, $result);
$this->assertSame('success', $result['status']);
$this->assertSame($user_data['email'], $result['email']);
$this->assertSame($user_data['name'], $result['name']);
$this->assertSame(true, $result['is_domain']);
$this->assertSame([], $result['domains']);
// Final signup request
$login = 'admin';
$domain = 'external.com';
$data = [
'login' => $login,
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup', $data);
$result = $response->json();
$response->assertStatus(200);
$this->assertCount(4, $result);
$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']);
// Check if the code has been removed
$this->assertNull(SignupCode::find($code->id));
// Check if the user has been created
$user = User::where('email', $login . '@' . $domain)->first();
$this->assertNotEmpty($user);
$this->assertSame($user_data['name'], $user->name);
// Check domain record
// Check external email in user settings
$this->assertSame($user_data['email'], $user->getSetting('external_email'));
// TODO: Check SKUs/Plan
// TODO: Check if the access token works
}
/**
* List of email address validation cases for testValidateEmail()
*
* @return array Arguments for testValidateEmail()
*/
public function dataValidateEmail()
{
return [
// invalid
['', 'validation.emailinvalid'],
['example.org', 'validation.emailinvalid'],
['@example.org', 'validation.emailinvalid'],
['test@localhost', 'validation.emailinvalid'],
// valid
['test@domain.tld', null],
['&@example.org', null],
];
}
/**
* Signup email validation.
*
* Note: Technicly these are unit tests, but let's keep it here for now.
* FIXME: Shall we do a http request for each case?
*
* @dataProvider dataValidateEmail
*/
public function testValidateEmail($email, $expected_result)
{
$method = new \ReflectionMethod('App\Http\Controllers\API\SignupController', 'validateEmail');
$method->setAccessible(true);
$result = $method->invoke(new SignupController(), $email);
$this->assertSame($expected_result, $result);
}
/**
* List of login/domain validation cases for testValidateLogin()
*
* @return array Arguments for testValidateLogin()
*/
public function dataValidateLogin()
{
$domain = $this->getPublicDomain();
return [
// Individual account
['', $domain, false, ['login' => 'validation.logininvalid']],
['test123456', 'localhost', false, ['domain' => 'validation.domaininvalid']],
['test123456', 'unknown-domain.org', false, ['domain' => 'validation.domaininvalid']],
['test.test', $domain, false, null],
['test_test', $domain, false, null],
['test-test', $domain, false, null],
['admin', $domain, false, ['login' => 'validation.loginexists']],
['administrator', $domain, false, ['login' => 'validation.loginexists']],
['sales', $domain, false, ['login' => 'validation.loginexists']],
['root', $domain, false, ['login' => 'validation.loginexists']],
// existing user
['SignupControllerTest1', $domain, false, ['login' => 'validation.loginexists']],
// Domain account
['admin', 'kolabsys.com', true, null],
['testnonsystemdomain', 'invalid', true, ['domain' => 'validation.domaininvalid']],
['testnonsystemdomain', '.com', true, ['domain' => 'validation.domaininvalid']],
// existing user
['SignupControllerTest1', $domain, true, ['domain' => 'validation.domainexists']],
];
}
/**
* Signup login/domain validation.
*
* Note: Technicly 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)
{
$method = new \ReflectionMethod('App\Http\Controllers\API\SignupController', 'validateLogin');
$method->setAccessible(true);
$result = $method->invoke(new SignupController(), $login, $domain, $external);
$this->assertSame($expected_result, $result);
}
}