+
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
+
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 307a5ce9..c6dd6bdd 100644
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -1,184 +1,186 @@
'api',
'prefix' => $prefix . 'api/auth'
],
function ($router) {
Route::post('login', 'API\AuthController@login');
Route::group(
['middleware' => 'auth:api'],
function ($router) {
Route::get('info', 'API\AuthController@info');
Route::post('logout', 'API\AuthController@logout');
Route::post('refresh', 'API\AuthController@refresh');
}
);
}
);
Route::group(
[
'domain' => \config('app.domain'),
'middleware' => 'api',
'prefix' => $prefix . 'api/auth'
],
function ($router) {
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::get('signup/invitations/{id}', 'API\SignupController@invitation');
+ Route::get('signup/plans', 'API\SignupController@plans');
Route::post('signup/verify', 'API\SignupController@verify');
Route::post('signup', 'API\SignupController@signup');
}
);
Route::group(
[
'domain' => \config('app.domain'),
'middleware' => 'auth:api',
'prefix' => $prefix . 'api/v4'
],
function () {
Route::apiResource('domains', API\V4\DomainsController::class);
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
Route::get('domains/{id}/status', 'API\V4\DomainsController@status');
Route::apiResource('packages', API\V4\PackagesController::class);
Route::apiResource('skus', API\V4\SkusController::class);
Route::apiResource('users', API\V4\UsersController::class);
Route::get('users/{id}/skus', 'API\V4\SkusController@userSkus');
Route::get('users/{id}/status', 'API\V4\UsersController@status');
Route::apiResource('wallets', API\V4\WalletsController::class);
Route::get('wallets/{id}/transactions', 'API\V4\WalletsController@transactions');
Route::get('wallets/{id}/receipts', 'API\V4\WalletsController@receipts');
Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\WalletsController@receiptDownload');
Route::post('payments', 'API\V4\PaymentsController@store');
Route::get('payments/mandate', 'API\V4\PaymentsController@mandate');
Route::post('payments/mandate', 'API\V4\PaymentsController@mandateCreate');
Route::put('payments/mandate', 'API\V4\PaymentsController@mandateUpdate');
Route::delete('payments/mandate', 'API\V4\PaymentsController@mandateDelete');
Route::get('openvidu/rooms', 'API\V4\OpenViduController@index');
Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom');
Route::post('openvidu/rooms/{id}/config', 'API\V4\OpenViduController@setRoomConfig');
// FIXME: I'm not sure about this one, should we use DELETE request maybe?
Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection');
Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection');
Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest');
Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest');
}
);
// Note: In Laravel 7.x we could just use withoutMiddleware() instead of a separate group
Route::group(
[
'domain' => \config('app.domain'),
'prefix' => $prefix . 'api/v4'
],
function () {
Route::post('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom');
Route::post('openvidu/rooms/{id}/connections', 'API\V4\OpenViduController@createConnection');
// FIXME: I'm not sure about this one, should we use DELETE request maybe?
Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection');
Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection');
Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest');
Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest');
}
);
Route::group(
[
'domain' => \config('app.domain'),
'middleware' => 'api',
'prefix' => $prefix . 'api/v4'
],
function ($router) {
Route::post('support/request', 'API\V4\SupportController@request');
}
);
Route::group(
[
'domain' => \config('app.domain'),
'prefix' => $prefix . 'api/webhooks',
],
function () {
Route::post('payment/{provider}', 'API\V4\PaymentsController@webhook');
Route::post('meet/openvidu', 'API\V4\OpenViduController@webhook');
}
);
Route::group(
[
'domain' => 'admin.' . \config('app.domain'),
'middleware' => ['auth:api', 'admin'],
'prefix' => $prefix . 'api/v4',
],
function () {
Route::apiResource('domains', API\V4\Admin\DomainsController::class);
Route::post('domains/{id}/suspend', 'API\V4\Admin\DomainsController@suspend');
Route::post('domains/{id}/unsuspend', 'API\V4\Admin\DomainsController@unsuspend');
Route::apiResource('packages', API\V4\Admin\PackagesController::class);
Route::apiResource('skus', API\V4\Admin\SkusController::class);
Route::apiResource('users', API\V4\Admin\UsersController::class);
Route::post('users/{id}/reset2FA', 'API\V4\Admin\UsersController@reset2FA');
Route::get('users/{id}/skus', 'API\V4\Admin\SkusController@userSkus');
Route::post('users/{id}/suspend', 'API\V4\Admin\UsersController@suspend');
Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend');
Route::apiResource('wallets', API\V4\Admin\WalletsController::class);
Route::post('wallets/{id}/one-off', 'API\V4\Admin\WalletsController@oneOff');
Route::get('wallets/{id}/transactions', 'API\V4\Admin\WalletsController@transactions');
Route::apiResource('discounts', API\V4\Admin\DiscountsController::class);
Route::get('stats/chart/{chart}', 'API\V4\Admin\StatsController@chart');
}
);
Route::group(
[
'domain' => 'reseller.' . \config('app.domain'),
'middleware' => ['auth:api', 'reseller'],
'prefix' => $prefix . 'api/v4',
],
function () {
Route::apiResource('domains', API\V4\Reseller\DomainsController::class);
Route::post('domains/{id}/suspend', 'API\V4\Reseller\DomainsController@suspend');
Route::post('domains/{id}/unsuspend', 'API\V4\Reseller\DomainsController@unsuspend');
-
+ Route::apiResource('invitations', API\V4\Reseller\InvitationsController::class);
+ Route::post('invitations/{id}/resend', 'API\V4\Reseller\InvitationsController@resend');
Route::apiResource('packages', API\V4\Reseller\PackagesController::class);
Route::apiResource('skus', API\V4\Reseller\SkusController::class);
Route::apiResource('users', API\V4\Reseller\UsersController::class);
Route::post('users/{id}/reset2FA', 'API\V4\Reseller\UsersController@reset2FA');
Route::get('users/{id}/skus', 'API\V4\Reseller\SkusController@userSkus');
Route::post('users/{id}/suspend', 'API\V4\Reseller\UsersController@suspend');
Route::post('users/{id}/unsuspend', 'API\V4\Reseller\UsersController@unsuspend');
Route::apiResource('wallets', API\V4\Reseller\WalletsController::class);
Route::post('wallets/{id}/one-off', 'API\V4\Reseller\WalletsController@oneOff');
Route::get('wallets/{id}/transactions', 'API\V4\Reseller\WalletsController@transactions');
Route::apiResource('discounts', API\V4\Reseller\DiscountsController::class);
}
);
diff --git a/src/tests/Browser/Pages/Reseller/Invitations.php b/src/tests/Browser/Pages/Reseller/Invitations.php
new file mode 100644
index 00000000..8837c21c
--- /dev/null
+++ b/src/tests/Browser/Pages/Reseller/Invitations.php
@@ -0,0 +1,49 @@
+assertPathIs($this->url())
+ ->waitUntilMissing('@app .app-loader')
+ ->assertSeeIn('#invitations .card-title', 'Signup Invitations');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@create-button' => '.card-text button.create-invite',
+ '@create-dialog' => '#invite-create',
+ '@search-button' => '#search-form button',
+ '@search-input' => '#search-form input',
+ '@table' => '#invitations-list',
+ ];
+ }
+}
diff --git a/src/tests/Browser/Reseller/InvitationsTest.php b/src/tests/Browser/Reseller/InvitationsTest.php
new file mode 100644
index 00000000..8d43bb90
--- /dev/null
+++ b/src/tests/Browser/Reseller/InvitationsTest.php
@@ -0,0 +1,222 @@
+browse(function (Browser $browser) {
+ $browser->visit('/invitations')->on(new Home());
+ });
+ }
+
+ /**
+ * Test Invitations creation
+ */
+ public function testInvitationCreate(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $date_regexp = '/^20[0-9]{2}-/';
+
+ $browser->visit(new Home())
+ ->submitLogon('reseller@kolabnow.com', 'reseller', true)
+ ->on(new Dashboard())
+ ->assertSeeIn('@links .link-invitations', 'Invitations')
+ ->click('@links .link-invitations')
+ ->on(new Invitations())
+ ->assertElementsCount('@table tbody tr', 0)
+ ->assertMissing('#more-loader')
+ ->assertSeeIn('@table tfoot td', "There are no invitations in the database.")
+ ->assertSeeIn('@create-button', 'Create invite(s)');
+
+ // Create a single invite with email address input
+ $browser->click('@create-button')
+ ->with(new Dialog('#invite-create'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Invite for a signup')
+ ->assertFocused('@body input#email')
+ ->assertValue('@body input#email', '')
+ ->type('@body input#email', 'test')
+ ->assertSeeIn('@button-action', 'Send invite(s)')
+ ->click('@button-action')
+ ->assertToast(Toast::TYPE_ERROR, "Form validation error")
+ ->waitFor('@body input#email.is-invalid')
+ ->assertSeeIn(
+ '@body input#email.is-invalid + .invalid-feedback',
+ "The email must be a valid email address."
+ )
+ ->type('@body input#email', 'test@domain.tld')
+ ->click('@button-action');
+ })
+ ->assertToast(Toast::TYPE_SUCCESS, "The invitation has been created.")
+ ->waitUntilMissing('#invite-create')
+ ->waitUntilMissing('@table .app-loader')
+ ->assertElementsCount('@table tbody tr', 1)
+ ->assertMissing('@table tfoot')
+ ->assertSeeIn('@table tbody tr td.email', 'test@domain.tld')
+ ->assertText('@table tbody tr td.email title', 'Not sent yet')
+ ->assertTextRegExp('@table tbody tr td.datetime', $date_regexp)
+ ->assertVisible('@table tbody tr td.buttons button.button-delete')
+ ->assertVisible('@table tbody tr td.buttons button.button-resend:disabled');
+
+ sleep(1);
+
+ // Create invites from a file
+ $browser->click('@create-button')
+ ->with(new Dialog('#invite-create'), function (Browser $browser) {
+ $browser->assertFocused('@body input#email')
+ ->assertValue('@body input#email', '')
+ ->assertMissing('@body input#email.is-invalid')
+ // Submit an empty file
+ ->attach('@body input#file', __DIR__ . '/../../data/empty.csv')
+ ->assertSeeIn('@body input#file + label', 'empty.csv')
+ ->click('@button-action')
+ ->assertToast(Toast::TYPE_ERROR, "Form validation error")
+ // ->waitFor('input#file.is-invalid')
+ ->assertSeeIn(
+ '@body input#file.is-invalid + label + .invalid-feedback',
+ "Failed to find any valid email addresses in the uploaded file."
+ )
+ // Submit non-empty file
+ ->attach('@body input#file', __DIR__ . '/../../data/email.csv')
+ ->click('@button-action');
+ })
+ ->assertToast(Toast::TYPE_SUCCESS, "2 invitations has been created.")
+ ->waitUntilMissing('#invite-create')
+ ->waitUntilMissing('@table .app-loader')
+ ->assertElementsCount('@table tbody tr', 3)
+ ->assertTextRegExp('@table tbody tr:nth-child(1) td.email', '/email[12]@test\.com$/')
+ ->assertTextRegExp('@table tbody tr:nth-child(2) td.email', '/email[12]@test\.com$/');
+ });
+ }
+
+ /**
+ * Test Invitations deletion and resending
+ */
+ public function testInvitationDeleteAndResend(): void
+ {
+ $this->browse(function (Browser $browser) {
+ Queue::fake();
+ $i1 = SignupInvitation::create(['email' => 'test1@domain.org']);
+ $i2 = SignupInvitation::create(['email' => 'test2@domain.org']);
+ SignupInvitation::where('id', $i2->id)
+ ->update(['created_at' => now()->subHours('2'), 'status' => SignupInvitation::STATUS_FAILED]);
+
+ // Test deleting
+ $browser->visit(new Invitations())
+ // ->submitLogon('reseller@kolabnow.com', 'reseller', true)
+ ->assertElementsCount('@table tbody tr', 2)
+ ->click('@table tbody tr:first-child button.button-delete')
+ ->assertToast(Toast::TYPE_SUCCESS, "Invitation deleted successfully.")
+ ->assertElementsCount('@table tbody tr', 1);
+
+ // Test resending
+ $browser->click('@table tbody tr:first-child button.button-resend')
+ ->assertToast(Toast::TYPE_SUCCESS, "Invitation added to the sending queue successfully.")
+ ->assertElementsCount('@table tbody tr', 1);
+ });
+ }
+
+ /**
+ * Test Invitations list (paging and searching)
+ */
+ public function testInvitationsList(): void
+ {
+ $this->browse(function (Browser $browser) {
+ Queue::fake();
+ $i1 = SignupInvitation::create(['email' => 'email1@ext.com']);
+ $i2 = SignupInvitation::create(['email' => 'email2@ext.com']);
+ $i3 = SignupInvitation::create(['email' => 'email3@ext.com']);
+ $i4 = SignupInvitation::create(['email' => 'email4@other.com']);
+ $i5 = SignupInvitation::create(['email' => 'email5@other.com']);
+ $i6 = SignupInvitation::create(['email' => 'email6@other.com']);
+ $i7 = SignupInvitation::create(['email' => 'email7@other.com']);
+ $i8 = SignupInvitation::create(['email' => 'email8@other.com']);
+ $i9 = SignupInvitation::create(['email' => 'email9@other.com']);
+ $i10 = SignupInvitation::create(['email' => 'email10@other.com']);
+ $i11 = SignupInvitation::create(['email' => 'email11@other.com']);
+
+ SignupInvitation::query()->update(['created_at' => now()->subDays('1')]);
+ SignupInvitation::where('id', $i1->id)
+ ->update(['created_at' => now()->subHours('2'), 'status' => SignupInvitation::STATUS_FAILED]);
+ SignupInvitation::where('id', $i2->id)
+ ->update(['created_at' => now()->subHours('3'), 'status' => SignupInvitation::STATUS_SENT]);
+ SignupInvitation::where('id', $i3->id)
+ ->update(['created_at' => now()->subHours('4'), 'status' => SignupInvitation::STATUS_COMPLETED]);
+ SignupInvitation::where('id', $i11->id)->update(['created_at' => now()->subDays('3')]);
+
+ // Test paging (load more) feature
+ $browser->visit(new Invitations())
+ // ->submitLogon('reseller@kolabnow.com', 'reseller', true)
+ ->assertElementsCount('@table tbody tr', 10)
+ ->assertSeeIn('#more-loader button', 'Load more')
+ ->with('@table tbody', function ($browser) use ($i1, $i2, $i3) {
+ $browser->assertSeeIn('tr:nth-child(1) td.email', $i1->email)
+ ->assertText('tr:nth-child(1) td.email svg.text-danger title', 'Sending failed')
+ ->assertVisible('tr:nth-child(1) td.buttons button.button-delete')
+ ->assertVisible('tr:nth-child(1) td.buttons button.button-resend:not(:disabled)')
+ ->assertSeeIn('tr:nth-child(2) td.email', $i2->email)
+ ->assertText('tr:nth-child(2) td.email svg.text-primary title', 'Sent')
+ ->assertVisible('tr:nth-child(2) td.buttons button.button-delete')
+ ->assertVisible('tr:nth-child(2) td.buttons button.button-resend:not(:disabled)')
+ ->assertSeeIn('tr:nth-child(3) td.email', $i3->email)
+ ->assertText('tr:nth-child(3) td.email svg.text-success title', 'User signed up')
+ ->assertVisible('tr:nth-child(3) td.buttons button.button-delete')
+ ->assertVisible('tr:nth-child(3) td.buttons button.button-resend:disabled')
+ ->assertText('tr:nth-child(4) td.email svg title', 'Not sent yet')
+ ->assertVisible('tr:nth-child(4) td.buttons button.button-delete')
+ ->assertVisible('tr:nth-child(4) td.buttons button.button-resend:disabled');
+ })
+ ->click('#more-loader button')
+ ->whenAvailable('@table tbody tr:nth-child(11)', function ($browser) use ($i11) {
+ $browser->assertSeeIn('td.email', $i11->email);
+ })
+ ->assertMissing('#more-loader button');
+
+ // Test searching (by domain)
+ $browser->type('@search-input', 'ext.com')
+ ->click('@search-button')
+ ->waitUntilMissing('@table .app-loader')
+ ->assertElementsCount('@table tbody tr', 3)
+ ->assertMissing('#more-loader button')
+ // search by full email
+ ->type('@search-input', 'email7@other.com')
+ ->keys('@search-input', '{enter}')
+ ->waitUntilMissing('@table .app-loader')
+ ->assertElementsCount('@table tbody tr', 1)
+ ->assertSeeIn('@table tbody tr:nth-child(1) td.email', 'email7@other.com')
+ ->assertMissing('#more-loader button')
+ // reset search
+ ->vueClear('#search-form input')
+ ->keys('@search-input', '{enter}')
+ ->waitUntilMissing('@table .app-loader')
+ ->assertElementsCount('@table tbody tr', 10)
+ ->assertVisible('#more-loader button');
+ });
+ }
+}
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
index eed0948d..31655561 100644
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -1,545 +1,634 @@
deleteTestUser('signuptestdusk@' . \config('app.domain'));
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
}
+ /**
+ * {@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();
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([
'data' => [
'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(['signup', 'explore', 'blog', 'support', 'login'], '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')
->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->assertVisible('#signup_last_name')
->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(['signup', 'explore', 'blog', 'support', 'login'], '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')
->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) {
- $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', '');
+ $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_confirm')
+ ->assertVisible('select#signup_domain')
+ ->assertElementsCount('select#signup_domain option', 13, false)
+ ->assertVisible('[type=button]')
+ ->assertVisible('[type=submit]')
+ ->assertSeeIn('[type=submit]', 'Submit')
+ ->assertFocused('#signup_login')
+ ->assertValue('select#signup_domain', \config('app.domain'))
+ ->assertValue('#signup_login', '')
+ ->assertValue('#signup_password', '')
+ ->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) {
$step->assertFocused('#signup_login')
->type('#signup_login', '*')
->type('#signup_password', '12345678')
->type('#signup_confirm', '123456789')
->click('[type=submit]')
->waitFor('#signup_login.is-invalid')
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password + .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 + .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_confirm', '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_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) {
$step->assertFocused('#signup_login')
->type('#signup_login', '*')
->type('#signup_domain', 'test.com')
->type('#signup_password', '12345678')
->type('#signup_confirm', '123456789')
->click('[type=submit]')
->waitFor('#signup_login.is-invalid')
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password + .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_confirm', '12345678')
->click('[type=submit]')
->waitUntilMissing('#signup_login.is-invalid')
->waitFor('#signup_domain.is-invalid + .invalid-feedback')
->assertMissing('#signup_password.is-invalid')
->assertMissing('#signup_password + .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 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_confirm', '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) {
+ $step->assertMissing('.card-title')
+ ->assertVisible('#signup_last_name')
+ ->assertVisible('#signup_first_name')
+ ->assertVisible('#signup_login')
+ ->assertVisible('#signup_password')
+ ->assertVisible('#signup_confirm')
+ ->assertVisible('select#signup_domain')
+ ->assertElementsCount('select#signup_domain option', 13, 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_confirm', '');
+
+ // Submit invalid data
+ $step->type('#signup_login', '*')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_confirm', '123456789')
+ ->click('[type=submit]')
+ ->waitFor('#signup_login.is-invalid')
+ ->assertVisible('#signup_domain + .invalid-feedback')
+ ->assertVisible('#signup_password.is-invalid')
+ ->assertVisible('#signup_password + .invalid-feedback')
+ ->assertFocused('#signup_login')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
+
+ // Submit valid data
+ $step->type('#signup_confirm', '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/Reseller/InvitationsTest.php b/src/tests/Feature/Controller/Reseller/InvitationsTest.php
new file mode 100644
index 00000000..bfc383ab
--- /dev/null
+++ b/src/tests/Feature/Controller/Reseller/InvitationsTest.php
@@ -0,0 +1,348 @@
+ 1]);
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test deleting invitations (DELETE /api/v4/invitations/
)
+ */
+ public function testDestroy(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $reseller = $this->getTestUser('reseller@reseller.com');
+ $reseller2 = $this->getTestUser('reseller@kolabnow.com');
+ $tenant = Tenant::where('title', 'Sample Tenant')->first();
+
+ \config(['app.tenant_id' => $tenant->id]);
+
+ $inv = SignupInvitation::create(['email' => 'email1@ext.com']);
+
+ // Non-admin user
+ $response = $this->actingAs($user)->delete("api/v4/invitations/{$inv->id}");
+ $response->assertStatus(403);
+
+ // Admin user
+ $response = $this->actingAs($admin)->delete("api/v4/invitations/{$inv->id}");
+ $response->assertStatus(403);
+
+ // Reseller user, but different tenant
+ $response = $this->actingAs($reseller2)->delete("api/v4/invitations/{$inv->id}");
+ $response->assertStatus(403);
+
+ // Reseller - non-existing invitation identifier
+ $response = $this->actingAs($reseller)->delete("api/v4/invitations/abd");
+ $response->assertStatus(404);
+
+ // Reseller - existing invitation
+ $response = $this->actingAs($reseller)->delete("api/v4/invitations/{$inv->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Invitation deleted successfully.", $json['message']);
+ $this->assertSame(null, SignupInvitation::find($inv->id));
+ }
+
+ /**
+ * Test listing invitations (GET /api/v4/invitations)
+ */
+ public function testIndex(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $reseller = $this->getTestUser('reseller@reseller.com');
+ $reseller2 = $this->getTestUser('reseller@kolabnow.com');
+ $tenant = Tenant::where('title', 'Sample Tenant')->first();
+
+ \config(['app.tenant_id' => $tenant->id]);
+
+ // Non-admin user
+ $response = $this->actingAs($user)->get("api/v4/invitations");
+ $response->assertStatus(403);
+
+ // Admin user
+ $response = $this->actingAs($admin)->get("api/v4/invitations");
+ $response->assertStatus(403);
+
+ // Reseller user, but different tenant
+ $response = $this->actingAs($reseller2)->get("api/v4/invitations");
+ $response->assertStatus(403);
+
+ // Reseller (empty list)
+ $response = $this->actingAs($reseller)->get("api/v4/invitations");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(0, $json['count']);
+ $this->assertSame([], $json['list']);
+ $this->assertSame(1, $json['page']);
+ $this->assertFalse($json['hasMore']);
+
+ // Add some invitations
+ $i1 = SignupInvitation::create(['email' => 'email1@ext.com']);
+ $i2 = SignupInvitation::create(['email' => 'email2@ext.com']);
+ $i3 = SignupInvitation::create(['email' => 'email3@ext.com']);
+ $i4 = SignupInvitation::create(['email' => 'email4@other.com']);
+ $i5 = SignupInvitation::create(['email' => 'email5@other.com']);
+ $i6 = SignupInvitation::create(['email' => 'email6@other.com']);
+ $i7 = SignupInvitation::create(['email' => 'email7@other.com']);
+ $i8 = SignupInvitation::create(['email' => 'email8@other.com']);
+ $i9 = SignupInvitation::create(['email' => 'email9@other.com']);
+ $i10 = SignupInvitation::create(['email' => 'email10@other.com']);
+ $i11 = SignupInvitation::create(['email' => 'email11@other.com']);
+ $i12 = SignupInvitation::create(['email' => 'email12@test.com']);
+ $i13 = SignupInvitation::create(['email' => 'email13@ext.com']);
+
+ SignupInvitation::query()->update(['created_at' => now()->subDays('1')]);
+ SignupInvitation::where('id', $i1->id)
+ ->update(['created_at' => now()->subHours('2'), 'status' => SignupInvitation::STATUS_FAILED]);
+ SignupInvitation::where('id', $i2->id)
+ ->update(['created_at' => now()->subHours('3'), 'status' => SignupInvitation::STATUS_SENT]);
+ SignupInvitation::where('id', $i11->id)->update(['created_at' => now()->subDays('3')]);
+ SignupInvitation::where('id', $i12->id)->update(['tenant_id' => 1]);
+ SignupInvitation::where('id', $i13->id)->update(['tenant_id' => 1]);
+
+ $response = $this->actingAs($reseller)->get("api/v4/invitations");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(10, $json['count']);
+ $this->assertSame(1, $json['page']);
+ $this->assertTrue($json['hasMore']);
+ $this->assertSame($i1->id, $json['list'][0]['id']);
+ $this->assertSame($i1->email, $json['list'][0]['email']);
+ $this->assertSame(true, $json['list'][0]['isFailed']);
+ $this->assertSame(false, $json['list'][0]['isNew']);
+ $this->assertSame(false, $json['list'][0]['isSent']);
+ $this->assertSame(false, $json['list'][0]['isCompleted']);
+ $this->assertSame($i2->id, $json['list'][1]['id']);
+ $this->assertSame($i2->email, $json['list'][1]['email']);
+ $this->assertFalse(in_array($i12->email, array_column($json['list'], 'email')));
+ $this->assertFalse(in_array($i13->email, array_column($json['list'], 'email')));
+
+ $response = $this->actingAs($reseller)->get("api/v4/invitations?page=2");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertSame(2, $json['page']);
+ $this->assertFalse($json['hasMore']);
+ $this->assertSame($i11->id, $json['list'][0]['id']);
+
+ // Test searching (email address)
+ $response = $this->actingAs($reseller)->get("api/v4/invitations?search=email3@ext.com");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertSame(1, $json['page']);
+ $this->assertFalse($json['hasMore']);
+ $this->assertSame($i3->id, $json['list'][0]['id']);
+
+ // Test searching (domain)
+ $response = $this->actingAs($reseller)->get("api/v4/invitations?search=ext.com");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(3, $json['count']);
+ $this->assertSame(1, $json['page']);
+ $this->assertFalse($json['hasMore']);
+ $this->assertSame($i1->id, $json['list'][0]['id']);
+ }
+
+ /**
+ * Test resending invitations (POST /api/v4/invitations//resend)
+ */
+ public function testResend(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $reseller = $this->getTestUser('reseller@reseller.com');
+ $reseller2 = $this->getTestUser('reseller@kolabnow.com');
+ $tenant = Tenant::where('title', 'Sample Tenant')->first();
+
+ \config(['app.tenant_id' => $tenant->id]);
+
+ $inv = SignupInvitation::create(['email' => 'email1@ext.com']);
+ SignupInvitation::where('id', $inv->id)->update(['status' => SignupInvitation::STATUS_FAILED]);
+
+ // Non-admin user
+ $response = $this->actingAs($user)->post("api/v4/invitations/{$inv->id}/resend");
+ $response->assertStatus(403);
+
+ // Admin user
+ $response = $this->actingAs($admin)->post("api/v4/invitations/{$inv->id}/resend");
+ $response->assertStatus(403);
+
+ // Reseller user, but different tenant
+ $response = $this->actingAs($reseller2)->post("api/v4/invitations/{$inv->id}/resend");
+ $response->assertStatus(403);
+
+ // Reseller - non-existing invitation identifier
+ $response = $this->actingAs($reseller)->post("api/v4/invitations/abd/resend");
+ $response->assertStatus(404);
+
+ // Reseller - existing invitation
+ $response = $this->actingAs($reseller)->post("api/v4/invitations/{$inv->id}/resend");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Invitation added to the sending queue successfully.", $json['message']);
+ $this->assertTrue($inv->fresh()->isNew());
+ }
+
+ /**
+ * Test creating invitations (POST /api/v4/invitations)
+ */
+ public function testStore(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $reseller = $this->getTestUser('reseller@reseller.com');
+ $reseller2 = $this->getTestUser('reseller@kolabnow.com');
+ $tenant = Tenant::where('title', 'Sample Tenant')->first();
+
+ \config(['app.tenant_id' => $tenant->id]);
+
+ // Non-admin user
+ $response = $this->actingAs($user)->post("api/v4/invitations", []);
+ $response->assertStatus(403);
+
+ // Admin user
+ $response = $this->actingAs($admin)->post("api/v4/invitations", []);
+ $response->assertStatus(403);
+
+ // Reseller user, but different tenant
+ $response = $this->actingAs($reseller2)->post("api/v4/invitations", []);
+ $response->assertStatus(403);
+
+ // Reseller (empty post)
+ $response = $this->actingAs($reseller)->post("api/v4/invitations", []);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame("The email field is required.", $json['errors']['email'][0]);
+
+ // Invalid email address
+ $post = ['email' => 'test'];
+ $response = $this->actingAs($reseller)->post("api/v4/invitations", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame("The email must be a valid email address.", $json['errors']['email'][0]);
+
+ // Valid email address
+ $post = ['email' => 'test@external.org'];
+ $response = $this->actingAs($reseller)->post("api/v4/invitations", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("The invitation has been created.", $json['message']);
+ $this->assertSame(1, $json['count']);
+ $this->assertSame(1, SignupInvitation::count());
+
+ // Test file input (empty file)
+ $tmpfile = tmpfile();
+ fwrite($tmpfile, "");
+ $file = new File('test.csv', $tmpfile);
+ $post = ['file' => $file];
+ $response = $this->actingAs($reseller)->post("api/v4/invitations", $post);
+
+ fclose($tmpfile);
+
+ $response->assertStatus(422);
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("Failed to find any valid email addresses in the uploaded file.", $json['errors']['file']);
+
+ // Test file input with an invalid email address
+ $tmpfile = tmpfile();
+ fwrite($tmpfile, "t1@domain.tld\r\nt2@domain");
+ $file = new File('test.csv', $tmpfile);
+ $post = ['file' => $file];
+ $response = $this->actingAs($reseller)->post("api/v4/invitations", $post);
+
+ fclose($tmpfile);
+
+ $response->assertStatus(422);
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("Found an invalid email address (t2@domain) on line 2.", $json['errors']['file']);
+
+ // Test file input (two addresses)
+ $tmpfile = tmpfile();
+ fwrite($tmpfile, "t1@domain.tld\r\nt2@domain.tld");
+ $file = new File('test.csv', $tmpfile);
+ $post = ['file' => $file];
+ $response = $this->actingAs($reseller)->post("api/v4/invitations", $post);
+
+ fclose($tmpfile);
+
+ $response->assertStatus(200);
+ $json = $response->json();
+
+ $this->assertSame(1, SignupInvitation::where('email', 't1@domain.tld')->count());
+ $this->assertSame(1, SignupInvitation::where('email', 't2@domain.tld')->count());
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("2 invitations has been created.", $json['message']);
+ $this->assertSame(2, $json['count']);
+ }
+}
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
index 704f2697..9905c646 100644
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -1,710 +1,782 @@
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();
}
/**
* {@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();
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 plans for signup
- *
- * @return void
*/
- public function testSignupPlans()
+ public function testSignupPlans(): void
{
$response = $this->get('/api/auth/signup/plans');
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertCount(2, $json['plans']);
$this->assertArrayHasKey('title', $json['plans'][0]);
$this->assertArrayHasKey('name', $json['plans'][0]);
$this->assertArrayHasKey('description', $json['plans'][0]);
$this->assertArrayHasKey('button', $json['plans'][0]);
}
+ /**
+ * 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
- *
- * @return void
*/
- public function testSignupInitInvalidInput()
+ 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']);
// TODO: Test phone validation
}
/**
* Test signup initialization with valid input
- *
- * @return array
*/
- public function testSignupInitValidInput()
+ 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);
$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) {
$code = TestCase::getObjectProperty($job, 'code');
return $code->code === $json['code']
&& $code->data['plan'] === $data['plan']
&& $code->data['email'] === $data['email']
&& $code->data['first_name'] === $data['first_name']
&& $code->data['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(2, $json);
$this->assertSame('success', $json['status']);
$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->data['plan'] === $data['plan']
&& $code->data['email'] === $data['email']
&& $code->data['voucher'] === $data['voucher']
&& $code->data['first_name'] === $data['first_name']
&& $code->data['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
- * @return void
*/
- public function testSignupVerifyInvalidInput(array $result)
+ 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
- *
- * @return array
*/
- public function testSignupVerifyValidInput(array $result)
+ public function testSignupVerifyValidInput(array $result): array
{
$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(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']));
return $result;
}
/**
* Test last signup step with invalid input
*
* @depends testSignupVerifyValidInput
- * @return void
*/
- public function testSignupInvalidInput(array $result)
+ 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
$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']);
$code = SignupCode::find($result['code']);
// Data with invalid voucher
$data = [
'login' => 'TestLogin',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'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' => '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)
+ public function testSignupValidInput(array $result): void
{
$queue = Queue::fake();
$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,
'voucher' => 'TEST',
];
$response = $this->post('/api/auth/signup', $data);
$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']);
}
);
// Check if the code has been removed
$this->assertNull(SignupCode::where('code', $result['code'])->first());
// Check if the user has been created
$user = User::where('email', $identity)->first();
$this->assertNotEmpty($user);
$this->assertSame($identity, $user->email);
// 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
- *
- * @return void
*/
- public function testSignupGroupAccount()
+ 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(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) {
$code = TestCase::getObjectProperty($job, 'code');
return $code->code === $json['code']
&& $code->data['plan'] === $data['plan']
&& $code->data['email'] === $data['email']
&& $code->data['first_name'] === $data['first_name']
&& $code->data['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' => '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->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
$this->assertNull(SignupCode::find($code->id));
// Check if the user has been created
$user = User::where('email', $login . '@' . $domain)->first();
$this->assertNotEmpty($user);
// 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 via invitation
+ */
+ public function testSignupViaInvitation(): 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' => 'test',
+ 'password_confirmation' => 'test',
+ ];
+
+ // 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
+ }
+
/**
* 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.']],
// TODO existing (public domain) user
// ['signuplogin', $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.']],
// existing custom domain
['jack', 'kolab.org', true, ['domain' => 'The specified domain is not available.']],
];
}
/**
* 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
*
* Note: Technically these include unit tests, but let's keep it here for now.
*/
public function testValidateLoginMore(): void
{
$group = $this->getTestGroup('group-test@kolabnow.com');
$login = 'group-test';
$domain = 'kolabnow.com';
$external = false;
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
$this->assertSame(['login' => 'The specified login is not available.'], $result);
}
}
diff --git a/src/tests/Feature/Jobs/SignupInvitationEmailTest.php b/src/tests/Feature/Jobs/SignupInvitationEmailTest.php
new file mode 100644
index 00000000..e91c7a3e
--- /dev/null
+++ b/src/tests/Feature/Jobs/SignupInvitationEmailTest.php
@@ -0,0 +1,68 @@
+invitation = SI::create(['email' => 'SignupInvitationEmailTest@external.com']);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->invitation->delete();
+ }
+
+ /**
+ * Test job handle
+ */
+ public function testSignupInvitationEmailHandle(): void
+ {
+ Mail::fake();
+
+ // Assert that no jobs were pushed...
+ Mail::assertNothingSent();
+
+ $job = new SignupInvitationEmail($this->invitation);
+ $job->handle();
+
+ // Assert the email sending job was pushed once
+ Mail::assertSent(SignupInvitation::class, 1);
+
+ // Assert the mail was sent to the code's email
+ Mail::assertSent(SignupInvitation::class, function ($mail) {
+ return $mail->hasTo($this->invitation->email);
+ });
+
+ $this->assertTrue($this->invitation->isSent());
+ }
+
+ /**
+ * Test job failure handling
+ */
+ public function testSignupInvitationEmailFailure(): void
+ {
+ $this->markTestIncomplete();
+ }
+}
diff --git a/src/tests/Feature/SignupInvitationTest.php b/src/tests/Feature/SignupInvitationTest.php
new file mode 100644
index 00000000..b32c63ad
--- /dev/null
+++ b/src/tests/Feature/SignupInvitationTest.php
@@ -0,0 +1,118 @@
+ 'test@domain.org']);
+
+ $this->assertSame('test@domain.org', $invitation->email);
+ $this->assertSame(SI::STATUS_NEW, $invitation->status);
+ $this->assertSame(\config('app.tenant_id'), $invitation->tenant_id);
+ $this->assertTrue(preg_match('/^[a-f0-9-]{36}$/', $invitation->id) > 0);
+
+ Queue::assertPushed(\App\Jobs\SignupInvitationEmail::class, 1);
+
+ Queue::assertPushed(
+ \App\Jobs\SignupInvitationEmail::class,
+ function ($job) use ($invitation) {
+ $inv = TestCase::getObjectProperty($job, 'invitation');
+
+ return $inv->id === $invitation->id && $inv->email === $invitation->email;
+ }
+ );
+
+ $inst = SI::find($invitation->id);
+
+ $this->assertInstanceOf(SI::class, $inst);
+ $this->assertSame($inst->id, $invitation->id);
+ $this->assertSame($inst->email, $invitation->email);
+ }
+
+ /**
+ * Test SignupInvitation update
+ */
+ public function testUpdate(): void
+ {
+ Queue::fake();
+
+ $invitation = SI::create(['email' => 'test@domain.org']);
+
+ Queue::fake();
+
+ // Test that these status changes do not dispatch the email sending job
+ foreach ([SI::STATUS_FAILED, SI::STATUS_SENT, SI::STATUS_COMPLETED, SI::STATUS_NEW] as $status) {
+ $invitation->status = $status;
+ $invitation->save();
+ }
+
+ Queue::assertNothingPushed();
+
+ // SENT -> NEW should resend the invitation
+ SI::where('id', $invitation->id)->update(['status' => SI::STATUS_SENT]);
+ $invitation->refresh();
+ $invitation->status = SI::STATUS_NEW;
+ $invitation->save();
+
+ Queue::assertPushed(\App\Jobs\SignupInvitationEmail::class, 1);
+
+ Queue::assertPushed(
+ \App\Jobs\SignupInvitationEmail::class,
+ function ($job) use ($invitation) {
+ $inv = TestCase::getObjectProperty($job, 'invitation');
+
+ return $inv->id === $invitation->id && $inv->email === $invitation->email;
+ }
+ );
+
+ Queue::fake();
+
+ // FAILED -> NEW should resend the invitation
+ SI::where('id', $invitation->id)->update(['status' => SI::STATUS_FAILED]);
+ $invitation->refresh();
+ $invitation->status = SI::STATUS_NEW;
+ $invitation->save();
+
+ Queue::assertPushed(\App\Jobs\SignupInvitationEmail::class, 1);
+
+ Queue::assertPushed(
+ \App\Jobs\SignupInvitationEmail::class,
+ function ($job) use ($invitation) {
+ $inv = TestCase::getObjectProperty($job, 'invitation');
+
+ return $inv->id === $invitation->id && $inv->email === $invitation->email;
+ }
+ );
+ }
+}
diff --git a/src/tests/Unit/Mail/SignupInvitationTest.php b/src/tests/Unit/Mail/SignupInvitationTest.php
new file mode 100644
index 00000000..c27b4188
--- /dev/null
+++ b/src/tests/Unit/Mail/SignupInvitationTest.php
@@ -0,0 +1,44 @@
+ 'abc',
+ 'email' => 'test@email',
+ ]);
+
+ $mail = $this->fakeMail(new SignupInvitation($invitation));
+
+ $html = $mail['html'];
+ $plain = $mail['plain'];
+
+ $url = Utils::serviceUrl('/signup/invite/' . $invitation->id);
+ $link = "$url";
+ $appName = \config('app.name');
+
+ $this->assertMailSubject("$appName Invitation", $mail['message']);
+
+ $this->assertStringStartsWith('', $html);
+ $this->assertTrue(strpos($html, $link) > 0);
+ $this->assertTrue(strpos($html, "invited to join $appName") > 0);
+
+ $this->assertStringStartsWith("Hi,", $plain);
+ $this->assertTrue(strpos($plain, "invited to join $appName") > 0);
+ $this->assertTrue(strpos($plain, $url) > 0);
+ }
+}
diff --git a/src/tests/Unit/SignupInvitationTest.php b/src/tests/Unit/SignupInvitationTest.php
new file mode 100644
index 00000000..640a4644
--- /dev/null
+++ b/src/tests/Unit/SignupInvitationTest.php
@@ -0,0 +1,35 @@
+status = $status;
+
+ $this->assertSame($status === SignupInvitation::STATUS_NEW, $invitation->isNew());
+ $this->assertSame($status === SignupInvitation::STATUS_SENT, $invitation->isSent());
+ $this->assertSame($status === SignupInvitation::STATUS_FAILED, $invitation->isFailed());
+ $this->assertSame($status === SignupInvitation::STATUS_COMPLETED, $invitation->isCompleted());
+ }
+ }
+}
diff --git a/src/tests/data/email.csv b/src/tests/data/email.csv
new file mode 100644
index 00000000..ed3af847
--- /dev/null
+++ b/src/tests/data/email.csv
@@ -0,0 +1,2 @@
+email1@test.com
+email2@test.com
diff --git a/src/tests/data/empty.csv b/src/tests/data/empty.csv
new file mode 100644
index 00000000..e69de29b