Page MenuHomePhorge

D3815.1775210849.diff
No OneTemporary

Authored By
Unknown
Size
33 KB
Referenced Files
None
Subscribers
None

D3815.1775210849.diff

diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -4,11 +4,11 @@
use App\Http\Controllers\Controller;
use App\Jobs\SignupVerificationEmail;
-use App\Jobs\SignupVerificationSMS;
use App\Discount;
use App\Domain;
use App\Plan;
use App\Rules\SignupExternalEmail;
+use App\Rules\SignupToken;
use App\Rules\Password;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
@@ -39,11 +39,18 @@
// Use reverse order just to have individual on left, group on right ;)
Plan::withEnvTenantContext()->orderByDesc('title')->get()
->map(function ($plan) use (&$plans) {
+ // Allow themes to set custom button label
+ $button = \trans('theme::app.planbutton-' . $plan->title);
+ if ($button == 'theme::app.planbutton-' . $plan->title) {
+ $button = \trans('app.planbutton', ['plan' => $plan->name]);
+ }
+
$plans[] = [
'title' => $plan->title,
'name' => $plan->name,
- 'button' => \trans('app.planbutton', ['plan' => $plan->name]),
+ 'button' => $button,
'description' => $plan->description,
+ 'mode' => $plan->mode ?: 'email',
];
});
@@ -62,49 +69,55 @@
*/
public function init(Request $request)
{
- // Check required fields
- $v = Validator::make(
- $request->all(),
- [
- 'email' => 'required',
- 'first_name' => 'max:128',
- 'last_name' => 'max:128',
- 'plan' => 'nullable|alpha_num|max:128',
- 'voucher' => 'max:32',
- ]
- );
+ $rules = [
+ 'first_name' => 'max:128',
+ 'last_name' => 'max:128',
+ 'voucher' => 'max:32',
+ ];
- $is_phone = false;
- $errors = $v->fails() ? $v->errors()->toArray() : [];
+ $plan = $this->getPlan();
- // Validate user email (or phone)
- if (empty($errors['email'])) {
- if ($error = $this->validatePhoneOrEmail($request->email, $is_phone)) {
- $errors['email'] = $error;
- }
+ if ($plan->mode == 'token') {
+ $rules['token'] = ['required', 'string', new SignupToken()];
+ } else {
+ $rules['email'] = ['required', 'string', new SignupExternalEmail()];
}
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ // Check required fields, validate input
+ $v = Validator::make($request->all(), $rules);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()->toArray()], 422);
}
// Generate the verification code
$code = SignupCode::create([
- 'email' => $request->email,
+ 'email' => $plan->mode == 'token' ? $request->token : $request->email,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
- 'plan' => $request->plan,
+ 'plan' => $plan->title,
'voucher' => $request->voucher,
]);
- // Send email/sms message
- if ($is_phone) {
- SignupVerificationSMS::dispatch($code);
+ $response = [
+ 'status' => 'success',
+ 'code' => $code->code,
+ 'mode' => $plan->mode ?: 'email',
+ ];
+
+ if ($plan->mode == 'token') {
+ // Token verification, jump to the last step
+ $has_domain = $plan->hasDomain();
+
+ $response['short_code'] = $code->short_code;
+ $response['is_domain'] = $has_domain;
+ $response['domains'] = $has_domain ? [] : Domain::getPublicDomains();
} else {
+ // External email verification, send an email message
SignupVerificationEmail::dispatch($code);
}
- return response()->json(['status' => 'success', 'code' => $code->code]);
+ return response()->json($response);
}
/**
@@ -251,10 +264,15 @@
$code_data = $v->getData();
$settings = [
- 'external_email' => $code_data->email,
'first_name' => $code_data->first_name,
'last_name' => $code_data->last_name,
];
+
+ if ($this->getPlan()->mode == 'token') {
+ $settings['signup_token'] = $code_data->email;
+ } else {
+ $settings['external_email'] = $code_data->email;
+ }
}
// Find the voucher discount
@@ -343,6 +361,8 @@
// Get the plan if specified and exists...
if ($request->code && $request->code->plan) {
$plan = Plan::withEnvTenantContext()->where('title', $request->code->plan)->first();
+ } elseif ($request->plan) {
+ $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
}
// ...otherwise use the default plan
@@ -358,40 +378,6 @@
}
/**
- * Checks if the input string is a valid email address or a phone number
- *
- * @param string $input Email address or phone number
- * @param bool $is_phone Will have been set to True if the string is valid phone number
- *
- * @return string Error message on validation error
- */
- protected static function validatePhoneOrEmail($input, &$is_phone = false): ?string
- {
- $is_phone = false;
-
- $v = Validator::make(
- ['email' => $input],
- ['email' => ['required', 'string', new SignupExternalEmail()]]
- );
-
- if ($v->fails()) {
- return $v->errors()->toArray()['email'][0];
- }
-
- // TODO: Phone number support
-/*
- $input = str_replace(array('-', ' '), '', $input);
-
- if (!preg_match('/^\+?[0-9]{9,12}$/', $input)) {
- return \trans('validation.noemailorphone');
- }
-
- $is_phone = true;
-*/
- return null;
- }
-
- /**
* Login (kolab identity) validation
*
* @param string $login Login (local part of an email address)
diff --git a/src/app/Http/Controllers/ContentController.php b/src/app/Http/Controllers/ContentController.php
--- a/src/app/Http/Controllers/ContentController.php
+++ b/src/app/Http/Controllers/ContentController.php
@@ -27,8 +27,6 @@
abort(404);
}
- self::loadLocale($theme);
-
return view($view)->with('env', \App\Utils::uiEnv());
}
@@ -63,8 +61,6 @@
// Localization
if (!empty($faq)) {
- self::loadLocale($theme_name);
-
foreach ($faq as $idx => $item) {
if (!empty($item['label'])) {
$faq[$idx]['title'] = \trans('theme::faq.' . $item['label']);
@@ -158,16 +154,4 @@
return $menu;
}
-
- /**
- * Register localization files from the theme.
- *
- * @param string $theme Theme name
- */
- protected static function loadLocale(string $theme): void
- {
- $path = resource_path(sprintf('themes/%s/lang', $theme));
-
- \app('translator')->addNamespace('theme', $path);
- }
}
diff --git a/src/app/Http/Middleware/Locale.php b/src/app/Http/Middleware/Locale.php
--- a/src/app/Http/Middleware/Locale.php
+++ b/src/app/Http/Middleware/Locale.php
@@ -64,6 +64,10 @@
app()->setLocale($lang);
}
+ // Allow skins to define/overwrite some localization
+ $theme = \config('app.theme');
+ \app('translator')->addNamespace('theme', \resource_path("themes/{$theme}/lang"));
+
return $next($request);
}
}
diff --git a/src/app/Observers/SignupCodeObserver.php b/src/app/Observers/SignupCodeObserver.php
--- a/src/app/Observers/SignupCodeObserver.php
+++ b/src/app/Observers/SignupCodeObserver.php
@@ -47,7 +47,7 @@
$code->expires_at = Carbon::now()->addHours($exp_hours);
$code->ip_address = request()->ip();
- if ($code->email) {
+ if ($code->email && strpos($code->email, '@')) {
$parts = explode('@', $code->email);
$code->local_part = $parts[0];
diff --git a/src/app/Plan.php b/src/app/Plan.php
--- a/src/app/Plan.php
+++ b/src/app/Plan.php
@@ -19,6 +19,7 @@
* @property int $discount_qty
* @property int $discount_rate
* @property string $id
+ * @property string $mode Plan signup mode (email|token)
* @property string $name
* @property \App\Package[] $packages
* @property datetime $promo_from
@@ -37,6 +38,7 @@
/** @var array<int, string> The attributes that are mass assignable */
protected $fillable = [
'title',
+ 'mode',
'name',
'description',
// a start and end datetime for this promotion
diff --git a/src/app/Rules/SignupToken.php b/src/app/Rules/SignupToken.php
new file mode 100644
--- /dev/null
+++ b/src/app/Rules/SignupToken.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Rules;
+
+use Illuminate\Contracts\Validation\Rule;
+
+class SignupToken implements Rule
+{
+ protected $message;
+
+ /**
+ * Determine if the validation rule passes.
+ *
+ * @param string $attribute Attribute name
+ * @param mixed $token The value to validate
+ *
+ * @return bool
+ */
+ public function passes($attribute, $token): bool
+ {
+ // Check the max length, according to the database column length
+ if (!is_string($token) || strlen($token) > 191) {
+ $this->message = \trans('validation.signuptokeninvalid');
+ return false;
+ }
+
+ // Check the list of tokens for token existence
+ $file = storage_path('signup-tokens.txt');
+ $list = [];
+ $token = \strtoupper($token);
+
+ if (file_exists($file)) {
+ $list = file($file);
+ $list = array_map('trim', $list);
+ $list = array_map('strtoupper', $list);
+ } else {
+ \Log::error("Signup tokens file ({$file}) does not exist");
+ }
+
+ if (!in_array($token, $list)) {
+ $this->message = \trans('validation.signuptokeninvalid');
+ return false;
+ }
+
+ // Check if the token has been already used for registration (exclude deleted users)
+ $used = \App\User::select()
+ ->join('user_settings', 'users.id', '=', 'user_settings.user_id')
+ ->where('user_settings.key', 'signup_token')
+ ->where('user_settings.value', $token)
+ ->exists();
+
+ if ($used) {
+ $this->message = \trans('validation.signuptokeninvalid');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the validation error message.
+ *
+ * @return string
+ */
+ public function message(): ?string
+ {
+ return $this->message;
+ }
+}
diff --git a/src/database/migrations/2022_09_01_100000_plans_mode.php b/src/database/migrations/2022_09_01_100000_plans_mode.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2022_09_01_100000_plans_mode.php
@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table(
+ 'plans',
+ function (Blueprint $table) {
+ $table->string('mode', 32)->default('email');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'plans',
+ function (Blueprint $table) {
+ $table->dropColumn('mode');
+ }
+ );
+ }
+};
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -381,6 +381,7 @@
'step1' => "Sign up to start your free month.",
'step2' => "We sent out a confirmation code to your email address. Enter the code we sent you, or click the link in the message.",
'step3' => "Create your Kolab identity (you can choose additional addresses later).",
+ 'token' => "Signup authorization token",
'voucher' => "Voucher Code",
],
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -170,6 +170,7 @@
'password-policy-min-len-error' => 'Minimum password length cannot be less than :min.',
'password-policy-max-len-error' => 'Maximum password length cannot be more than :max.',
'password-policy-last-error' => 'The minimum value for last N passwords is :last.',
+ 'signuptokeninvalid' => 'The signup token is invalid.',
/*
|--------------------------------------------------------------------------
diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss
--- a/src/resources/themes/app.scss
+++ b/src/resources/themes/app.scss
@@ -229,8 +229,8 @@
.plan-ico {
margin:auto;
font-size: 3.8rem;
- color: #f1a539;
- border: 3px solid #f1a539;
+ color: $main-color;
+ border: 3px solid $main-color;
width: 6rem;
height: 6rem;
border-radius: 50%;
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -20,7 +20,7 @@
<div class="card d-none" id="step1" v-if="!invitation">
<div class="card-body">
- <h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 1, n: 3 }) }}</h4>
+ <h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 1, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step1') }}
</p>
@@ -31,7 +31,11 @@
<input type="text" class="form-control rounded-end" id="signup_last_name" :placeholder="$t('form.surname')" v-model="last_name">
</div>
</div>
- <div class="mb-3">
+ <div v-if="mode == 'token'" class="mb-3">
+ <label for="signup_token" class="visually-hidden">{{ $t('signup.token') }}</label>
+ <input type="text" class="form-control" id="signup_token" :placeholder="$t('signup.token')" required v-model="token">
+ </div>
+ <div v-else class="mb-3">
<label for="signup_email" class="visually-hidden">{{ $t('signup.email') }}</label>
<input type="text" class="form-control" id="signup_email" :placeholder="$t('signup.email')" required v-model="email">
</div>
@@ -43,7 +47,7 @@
<div class="card d-none" id="step2" v-if="!invitation">
<div class="card-body">
- <h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 2, n: 3 }) }}</h4>
+ <h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 2, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step2') }}
</p>
@@ -61,7 +65,7 @@
<div class="card d-none" id="step3">
<div class="card-body">
- <h4 v-if="!invitation" class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 3, n: 3 }) }}</h4>
+ <h4 v-if="!invitation" class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: steps, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step3') }}
</p>
@@ -105,7 +109,8 @@
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
- require('@fortawesome/free-solid-svg-icons/faUsers').definition,
+ require('@fortawesome/free-solid-svg-icons/faMobileRetro').definition,
+ require('@fortawesome/free-solid-svg-icons/faUsers').definition
)
export default {
@@ -125,15 +130,23 @@
domains: [],
invitation: null,
is_domain: false,
+ mode: 'email',
plan: null,
plan_icons: {
individual: 'user',
- group: 'users'
+ group: 'users',
+ phone: 'mobile-retro'
},
plans: [],
+ token: '',
voucher: ''
}
},
+ computed: {
+ steps() {
+ return this.mode == 'token' ? 2 : 3
+ }
+ },
mounted() {
let param = this.$route.params.param;
@@ -165,8 +178,7 @@
this.submitStep2(true)
} else if (/^([a-zA-Z_]+)$/.test(param)) {
// Plan title provided, save it and display Step 1
- this.plan = param
- this.displayForm(1, true)
+ this.step0(param)
} else {
this.$root.errorPage(404)
}
@@ -177,30 +189,48 @@
methods: {
selectPlan(plan) {
this.$router.push({path: '/signup/' + plan})
- this.plan = plan
- this.displayForm(1, true)
+ this.selectPlanByTitle(plan)
},
// Composes plan selection page
- step0() {
+ selectPlanByTitle(title) {
+ const plan = this.plans.filter(plan => plan.title == title)[0]
+ if (plan) {
+ this.plan = title
+ this.mode = plan.mode
+ this.displayForm(1, true)
+ }
+ },
+ step0(plan) {
if (!this.plans.length) {
axios.get('/api/auth/signup/plans', { loader: true }).then(response => {
this.plans = response.data.plans
+ this.selectPlanByTitle(plan)
})
.catch(error => {
this.$root.errorHandler(error)
})
+ } else {
+ this.selectPlanByTitle(plan)
}
},
// Submits data to the API, validates and gets verification code
submitStep1() {
this.$root.clearFormValidation($('#step1 form'))
- const post = this.$root.pick(this, ['email', 'last_name', 'first_name', 'plan', 'voucher'])
+ const post = this.$root.pick(this, ['email', 'last_name', 'first_name', 'plan', 'token', 'voucher'])
axios.post('/api/auth/signup/init', post)
.then(response => {
- this.displayForm(2, true)
this.code = response.data.code
+ this.short_code = response.data.short_code
+ this.mode = response.data.mode
+ this.is_domain = response.data.is_domain
+ this.displayForm(this.mode == 'token' ? 3 : 2, true)
+
+ // Fill the domain selector with available domains
+ if (!this.is_domain) {
+ this.setDomain(response.data)
+ }
})
},
// Submits the code to the API for verification
@@ -222,6 +252,7 @@
this.email = response.data.email
this.is_domain = response.data.is_domain
this.voucher = response.data.voucher
+ this.domain = ''
// Fill the domain selector with available domains
if (!this.is_domain) {
@@ -262,12 +293,20 @@
},
// Moves the user a step back in registration form
stepBack(e) {
- var card = $(e.target).closest('.card')
+ const card = $(e.target).closest('.card')
+ let step = card.attr('id').replace('step', '')
- card.prev().removeClass('d-none').find('input').first().focus()
card.addClass('d-none').find('form')[0].reset()
- if (card.attr('id') == 'step1') {
+ step -= 1
+
+ if (step == 2 && this.mode == 'token') {
+ step = 1
+ }
+
+ $('#step' + step).removeClass('d-none').find('input').first().focus()
+
+ if (!step) {
this.step0()
this.$router.replace({path: '/signup'})
}
@@ -281,7 +320,7 @@
return this.step0()
}
- $('#step' + step).removeClass('d-none')
+ $('#step' + step).removeClass('d-none').find('form')[0].reset()
if (focus) {
$('#step' + step).find('input').first().focus()
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -4,6 +4,7 @@
use App\Discount;
use App\Domain;
+use App\Plan;
use App\SignupCode;
use App\SignupInvitation;
use App\User;
@@ -27,6 +28,8 @@
$this->deleteTestUser('signuptestdusk@' . \config('app.domain'));
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
+
+ Plan::where('mode', 'token')->update(['mode' => 'email']);
}
/**
@@ -39,6 +42,10 @@
$this->deleteTestDomain('user-domain-signup.com');
SignupInvitation::truncate();
+ Plan::where('mode', 'token')->update(['mode' => 'email']);
+
+ @unlink(storage_path('signup-tokens.txt'));
+
parent::tearDown();
}
@@ -125,6 +132,7 @@
$browser->waitForLocation('/signup/individual')
->assertVisible('@step1')
+ ->assertSeeIn('.card-title', 'Sign Up - Step 1/3')
->assertMissing('@step0')
->assertMissing('@step2')
->assertMissing('@step3')
@@ -162,7 +170,8 @@
// Here we expect two text inputs and Back and Continue buttons
$browser->with('@step1', function ($step) {
- $step->assertVisible('#signup_last_name')
+ $step->waitFor('#signup_last_name')
+ ->assertSeeIn('.card-title', 'Sign Up - Step 1/3')
->assertVisible('#signup_first_name')
->assertFocused('#signup_first_name')
->assertVisible('#signup_email')
@@ -221,6 +230,7 @@
{
$this->browse(function (Browser $browser) {
$browser->assertVisible('@step2')
+ ->assertSeeIn('@step2 .card-title', 'Sign Up - Step 2/3')
->assertMissing('@step0')
->assertMissing('@step1')
->assertMissing('@step3');
@@ -509,6 +519,109 @@
}
/**
+ * Test signup with a token plan
+ */
+ public function testSignupToken(): void
+ {
+ // Test the individual plan
+ Plan::where('title', 'individual')->update(['mode' => 'token']);
+
+ // Register some valid tokens
+ $tokens = ['1234567890', 'abcdefghijk'];
+ file_put_contents(storage_path('signup-tokens.txt'), implode("\n", $tokens));
+
+ $this->browse(function (Browser $browser) use ($tokens) {
+ $browser->visit(new Signup())
+ ->waitFor('@step0 .plan-individual button')
+ ->click('@step0 .plan-individual button')
+ // Step 1
+ ->whenAvailable('@step1', function ($browser) use ($tokens) {
+ $browser->assertSeeIn('.card-title', 'Sign Up - Step 1/2')
+ ->type('#signup_first_name', 'Test')
+ ->type('#signup_last_name', 'User')
+ ->assertMissing('#signup_email')
+ ->type('#signup_token', '1234')
+ // invalid token
+ ->click('[type=submit]')
+ ->waitFor('#signup_token.is-invalid')
+ ->assertVisible('#signup_token + .invalid-feedback')
+ ->assertFocused('#signup_token')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ // valid token
+ ->type('#signup_token', $tokens[0])
+ ->click('[type=submit]');
+ })
+ // Step 2
+ ->whenAvailable('@step3', function ($browser) {
+ $domains = Domain::getPublicDomains();
+ $domains_count = count($domains);
+
+ $browser->assertSeeIn('.card-title', 'Sign Up - Step 2/2')
+ ->assertElementsCount('select#signup_domain option', $domains_count, false)
+ ->assertText('select#signup_domain option:nth-child(1)', $domains[0])
+ ->assertValue('select#signup_domain option:nth-child(1)', $domains[0])
+ ->type('#signup_login', 'signuptestdusk')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_password_confirmation', '12345678')
+ ->click('[type=submit]');
+ })
+ ->waitUntilMissing('@step3')
+ ->on(new Dashboard())
+ ->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
+ });
+
+ $user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first();
+ $this->assertSame($tokens[0], $user->getSetting('signup_token'));
+ $this->assertSame(null, $user->getSetting('external_email'));
+
+ // Test the group plan
+ Plan::where('title', 'group')->update(['mode' => 'token']);
+
+ $this->browse(function (Browser $browser) use ($tokens) {
+ $browser->visit(new Signup())
+ ->waitFor('@step0 .plan-group button')
+ ->click('@step0 .plan-group button')
+ // Step 1
+ ->whenAvailable('@step1', function ($browser) use ($tokens) {
+ $browser->assertSeeIn('.card-title', 'Sign Up - Step 1/2')
+ ->type('#signup_first_name', 'Test')
+ ->type('#signup_last_name', 'User')
+ ->assertMissing('#signup_email')
+ ->type('#signup_token', '1234')
+ // invalid token
+ ->click('[type=submit]')
+ ->waitFor('#signup_token.is-invalid')
+ ->assertVisible('#signup_token + .invalid-feedback')
+ ->assertFocused('#signup_token')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ // valid token
+ ->type('#signup_token', $tokens[1])
+ ->click('[type=submit]');
+ })
+ // Step 2
+ ->whenAvailable('@step3', function ($browser) {
+ $browser->assertSeeIn('.card-title', 'Sign Up - Step 2/2')
+ ->type('input#signup_domain', 'user-domain-signup.com')
+ ->type('#signup_login', 'admin')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_password_confirmation', '12345678')
+ ->click('[type=submit]');
+ })
+ ->waitUntilMissing('@step3')
+ ->on(new Dashboard())
+ ->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
+ });
+
+ $user = User::where('email', 'admin@user-domain-signup.com')->first();
+ $this->assertSame($tokens[1], $user->getSetting('signup_token'));
+ $this->assertSame(null, $user->getSetting('external_email'));
+ }
+
+ /**
* Test signup with voucher
*/
public function testSignupVoucherLink(): void
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -204,7 +204,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $this->assertSame("The specified email address is invalid.", $json['errors']['email']);
+ $this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
SignupCode::truncate();
@@ -231,7 +231,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
// TODO: This probably should be a different message?
- $this->assertSame("The specified email address is invalid.", $json['errors']['email']);
+ $this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
// IP address limit check
$data = [
@@ -258,7 +258,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
// TODO: This probably should be a different message?
- $this->assertSame("The specified email address is invalid.", $json['errors']['email']);
+ $this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
// TODO: Test phone validation
}
@@ -284,8 +284,10 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(2, $json);
+
+ $this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
+ $this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
// Assert the email sending job was pushed once
@@ -309,8 +311,9 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(2, $json);
+ $this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
+ $this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
// Assert the job has proper data assigned
@@ -629,8 +632,9 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(2, $json);
+ $this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
+ $this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
// Assert the email sending job was pushed once
diff --git a/src/tests/Feature/PlanTest.php b/src/tests/Feature/PlanTest.php
--- a/src/tests/Feature/PlanTest.php
+++ b/src/tests/Feature/PlanTest.php
@@ -107,6 +107,9 @@
$this->assertTrue($plan->cost() == $package_costs);
}
+ /**
+ * Tests for Plan::tenant()
+ */
public function testTenant(): void
{
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
diff --git a/src/tests/Unit/Rules/SignupTokenTest.php b/src/tests/Unit/Rules/SignupTokenTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Rules/SignupTokenTest.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Tests\Unit\Rules;
+
+use App\Rules\SignupToken;
+use Illuminate\Support\Facades\Validator;
+use Tests\TestCase;
+
+class SignupTokenTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ @unlink(storage_path('signup-tokens.txt'));
+
+ $john = $this->getTestUser('john@kolab.org');
+ $john->settings()->where('key', 'signup_token')->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Tests the resource name validator
+ */
+ public function testValidation(): void
+ {
+ $tokens = ['1234567890', 'abcdefghijk'];
+ file_put_contents(storage_path('signup-tokens.txt'), implode("\n", $tokens));
+
+ $rules = ['token' => [new SignupToken()]];
+
+ // Empty input
+ $v = Validator::make(['token' => null], $rules);
+ $this->assertSame(['token' => ["The signup token is invalid."]], $v->errors()->toArray());
+
+ // Length limit
+ $v = Validator::make(['token' => str_repeat('a', 192)], $rules);
+ $this->assertSame(['token' => ["The signup token is invalid."]], $v->errors()->toArray());
+
+ // Non-existing token
+ $v = Validator::make(['token' => '123'], $rules);
+ $this->assertSame(['token' => ["The signup token is invalid."]], $v->errors()->toArray());
+
+ // Valid tokens
+ $v = Validator::make(['token' => $tokens[0]], $rules);
+ $this->assertSame([], $v->errors()->toArray());
+
+ $v = Validator::make(['token' => strtoupper($tokens[1])], $rules);
+ $this->assertSame([], $v->errors()->toArray());
+
+ // Tokens already used
+ $john = $this->getTestUser('john@kolab.org');
+ $john->setSetting('signup_token', $tokens[0]);
+
+ $v = Validator::make(['token' => $tokens[0]], $rules);
+ $this->assertSame(['token' => ["The signup token is invalid."]], $v->errors()->toArray());
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 10:07 AM (2 d, 11 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823604
Default Alt Text
D3815.1775210849.diff (33 KB)

Event Timeline