diff --git a/phpcs.xml b/phpcs.xml
index 0e7dd587..ac7cf195 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -1,12 +1,12 @@
- Custom ruleset for Kolab
+ Custom ruleset for Kolab
-
+
src/app/
src/tests/
diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php
index d5022e91..e4ed659f 100644
--- a/src/app/Backends/LDAP.php
+++ b/src/app/Backends/LDAP.php
@@ -1,15 +1,16 @@
morphOne('App\Entitlement', 'entitleable');
}
/**
* Returns whether this domain is active.
*
* @return bool
*/
public function isActive(): bool
{
return $this->status & self::STATUS_ACTIVE;
}
/**
* Returns whether this domain is confirmed the ownership of.
*
* @return bool
*/
public function isConfirmed(): bool
{
return $this->status & self::STATUS_CONFIRMED;
}
/**
* Returns whether this domain is deleted.
*
* @return bool
*/
public function isDeleted(): bool
{
return $this->status & self::STATUS_DELETED;
}
/**
* Returns whether this domain is registered with us.
*
* @return bool
*/
public function isExternal(): bool
{
return $this->type & self::TYPE_EXTERNAL;
}
/**
* Returns whether this domain is hosted with us.
*
* @return bool
*/
public function isHosted(): bool
{
return $this->type & self::TYPE_HOSTED;
}
/**
* Returns whether this domain is new.
*
* @return bool
*/
public function isNew(): bool
{
return $this->status & self::STATUS_NEW;
}
/**
* Returns whether this domain is public.
*
* @return bool
*/
public function isPublic(): bool
{
return $this->type & self::TYPE_PUBLIC;
}
/**
* Returns whether this domain is suspended.
*
* @return bool
*/
public function isSuspended(): bool
{
return $this->status & self::STATUS_SUSPENDED;
}
/*
public function setStatusAttribute($status)
{
$_status = $this->status;
switch ($status) {
case "new":
$_status += self::STATUS_NEW;
break;
case "active":
$_status += self::STATUS_ACTIVE;
$_status -= self::STATUS_NEW;
break;
case "confirmed":
$_status += self::STATUS_CONFIRMED;
$_status -= self::STATUS_NEW;
break;
case "suspended":
$_status += self::STATUS_SUSPENDED;
break;
case "deleted":
$_status += self::STATUS_DELETED;
break;
default:
$_status = $status;
//throw new \Exception("Invalid domain status: {$status}");
break;
}
$this->status = $_status;
}
*/
}
diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php
index 45ca0b36..c7e9da27 100644
--- a/src/app/Entitlement.php
+++ b/src/app/Entitlement.php
@@ -1,80 +1,81 @@
morphTo();
}
/**
* The SKU concerned.
*
* @return Sku
*/
public function sku()
{
return $this->belongsTo('App\Sku');
}
/**
* The owner of this entitlement.
*
* @return User
*/
public function owner()
{
return $this->belongsTo('App\User', 'owner_id', 'id');
}
/**
* The wallet this entitlement is being billed to
*
* @return Wallet
*/
public function wallet()
{
return $this->belongsTo('App\Wallet');
}
}
diff --git a/src/app/Handlers/Domain.php b/src/app/Handlers/Domain.php
index d2ff3aae..c8db9f67 100644
--- a/src/app/Handlers/Domain.php
+++ b/src/app/Handlers/Domain.php
@@ -1,16 +1,17 @@
sku_id)->active) {
return false;
}
return true;
}
}
diff --git a/src/app/Handlers/DomainHosting.php b/src/app/Handlers/DomainHosting.php
index 61f9ae8c..78c1d647 100644
--- a/src/app/Handlers/DomainHosting.php
+++ b/src/app/Handlers/DomainHosting.php
@@ -1,16 +1,17 @@
sku_id)->active) {
return false;
}
return false;
}
}
diff --git a/src/app/Handlers/DomainRegistration.php b/src/app/Handlers/DomainRegistration.php
index 903b0c4c..ff5494a5 100644
--- a/src/app/Handlers/DomainRegistration.php
+++ b/src/app/Handlers/DomainRegistration.php
@@ -1,16 +1,17 @@
sku_id)->active) {
return false;
}
return false;
}
}
diff --git a/src/app/Handlers/Groupware.php b/src/app/Handlers/Groupware.php
index 80323c75..fd6d97dc 100644
--- a/src/app/Handlers/Groupware.php
+++ b/src/app/Handlers/Groupware.php
@@ -1,16 +1,17 @@
sku_id)->active) {
return false;
}
return true;
}
}
diff --git a/src/app/Handlers/Resource.php b/src/app/Handlers/Resource.php
index babc4dac..51da6b15 100644
--- a/src/app/Handlers/Resource.php
+++ b/src/app/Handlers/Resource.php
@@ -1,16 +1,17 @@
sku_id)->active) {
return false;
}
return true;
}
}
diff --git a/src/app/Handlers/SharedFolder.php b/src/app/Handlers/SharedFolder.php
index afe23278..a30905a4 100644
--- a/src/app/Handlers/SharedFolder.php
+++ b/src/app/Handlers/SharedFolder.php
@@ -1,16 +1,17 @@
sku_id)->active) {
return false;
}
return true;
}
}
diff --git a/src/app/Handlers/Storage.php b/src/app/Handlers/Storage.php
index c39b040b..631e9ca6 100644
--- a/src/app/Handlers/Storage.php
+++ b/src/app/Handlers/Storage.php
@@ -1,16 +1,17 @@
sku_id)->active) {
return false;
}
return true;
}
}
diff --git a/src/app/Http/Controllers/API/PasswordResetController.php b/src/app/Http/Controllers/API/PasswordResetController.php
new file mode 100644
index 00000000..dcef6805
--- /dev/null
+++ b/src/app/Http/Controllers/API/PasswordResetController.php
@@ -0,0 +1,144 @@
+all(), ['email' => 'required|email']);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ // Find a user by email
+ $user = User::findByEmail($request->email);
+
+ if (!$user) {
+ $errors = ['email' => __('validation.usernotexists')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ if (!$user->getSetting('external_email')) {
+ $errors = ['email' => __('validation.noextemail')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ // Generate the verification code
+ $code = new VerificationCode(['mode' => 'password-reset']);
+ $user->verificationcodes()->save($code);
+
+ // Send email/sms message
+ PasswordResetEmail::dispatch($code);
+
+ return response()->json(['status' => 'success', 'code' => $code->code]);
+ }
+
+ /**
+ * Validation of the verification code.
+ *
+ * @param Illuminate\Http\Request HTTP request
+ *
+ * @return \Illuminate\Http\JsonResponse JSON response
+ */
+ public function verify(Request $request)
+ {
+ // Validate the request args
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'code' => 'required',
+ 'short_code' => 'required',
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ // Validate the verification code
+ $code = VerificationCode::find($request->code);
+
+ if (
+ empty($code)
+ || $code->isExpired()
+ || $code->mode !== 'password-reset'
+ || $code->short_code !== $request->short_code
+ ) {
+ $errors = ['short_code' => "The code is invalid or expired."];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ // For last-step remember the code object, so we can delete it
+ // with single SQL query (->delete()) instead of two (::destroy())
+ $this->code = $code;
+
+ // Return user name and email/phone from the codes database on success
+ return response()->json(['status' => 'success']);
+ }
+
+ /**
+ * Password change
+ *
+ * @param Illuminate\Http\Request HTTP request
+ *
+ * @return \Illuminate\Http\JsonResponse JSON response
+ */
+ public function reset(Request $request)
+ {
+ // Validate the request args
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'password' => 'required|min:4|confirmed',
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ $v = $this->verify($request);
+ if ($v->status() !== 200) {
+ return $v;
+ }
+
+ $user = $this->code->user;
+
+ // Change the user password
+ $user->setPasswordAttribute($request->password);
+ $user->save();
+
+ // Remove the verification code
+ $this->code->delete();
+
+ return UsersController::logonResponse($user);
+ }
+}
diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
index 8878b23d..6db8ffec 100644
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -1,263 +1,256 @@
all(),
[
'email' => 'required',
'name' => 'required',
]
);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
// Validate user email (or phone)
if ($error = $this->validatePhoneOrEmail($request->email, $is_phone)) {
return response()->json(['status' => 'error', 'errors' => ['email' => __($error)]], 422);
}
// Generate the verification code
$code = SignupCode::create([
'data' => [
'email' => $request->email,
'name' => $request->name,
]
]);
// Send email/sms message
if ($is_phone) {
SignupVerificationSMS::dispatch($code);
} else {
SignupVerificationEmail::dispatch($code);
}
return response()->json(['status' => 'success', 'code' => $code->code]);
}
/**
* Validation of the verification code.
*
* @param Illuminate\Http\Request HTTP request
*
* @return \Illuminate\Http\JsonResponse JSON response
*/
public function verify(Request $request)
{
// Validate the request args
$v = Validator::make(
$request->all(),
[
'code' => 'required',
'short_code' => 'required',
]
);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
// Validate the verification code
$code = SignupCode::find($request->code);
- if (empty($code)
+ if (
+ empty($code)
|| $code->isExpired()
|| Str::upper($request->short_code) !== Str::upper($code->short_code)
) {
$errors = ['short_code' => "The code is invalid or expired."];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
// For signup last-step mode remember the code object, so we can delete it
// with single SQL query (->delete()) instead of two (::destroy())
$this->code = $code;
// Return user name and email/phone from the codes database on success
return response()->json([
'status' => 'success',
'email' => $code->data['email'],
'name' => $code->data['name'],
]);
}
/**
* Finishes the signup process by creating the user account.
*
* @param Illuminate\Http\Request HTTP request
*
* @return \Illuminate\Http\JsonResponse JSON response
*/
public function signup(Request $request)
{
// Validate input
$v = Validator::make(
$request->all(),
[
'login' => 'required|min:2',
'password' => 'required|min:4|confirmed',
]
);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
$login = $request->login . '@' . \config('app.domain');
// Validate login (email)
if ($error = $this->validateEmail($login, true)) {
return response()->json(['status' => 'error', 'errors' => ['login' => $error]], 422);
}
// Validate verification codes (again)
$v = $this->verify($request);
if ($v->status() !== 200) {
return $v;
}
// Get user name/email from the verification code database
$code_data = $v->getData();
$user_name = $code_data->name;
$user_email = $code_data->email;
// We allow only ASCII, so we can safely lower-case the email address
$login = Str::lower($login);
$user = User::create(
[
'name' => $user_name,
'email' => $login,
'password' => $request->password,
]
);
// Save the external email in user settings
$user->setSettings(['external_email' => $user_email]);
// Remove the verification code
$this->code->delete();
- $token = auth()->login($user);
-
- return response()->json([
- 'status' => 'success',
- 'access_token' => $token,
- 'token_type' => 'bearer',
- 'expires_in' => Auth::guard()->factory()->getTTL() * 60,
- ]);
+ return UsersController::logonResponse($user);
}
/**
* Checks if the input string is a valid email address or a phone number
*
* @param string $email Email address or phone number
* @param bool &$is_phone Will be set to True if the string is valid phone number
*
* @return string Error message label on validation error
*/
protected function validatePhoneOrEmail($input, &$is_phone = false)
{
$is_phone = false;
return $this->validateEmail($input);
// TODO: Phone number support
/*
if (strpos($input, '@')) {
return $this->validateEmail($input);
}
$input = str_replace(array('-', ' '), '', $input);
if (!preg_match('/^\+?[0-9]{9,12}$/', $input)) {
return 'validation.noemailorphone';
}
$is_phone = true;
*/
}
/**
* Email address validation
*
* @param string $email Email address
* @param bool $signup Enables additional checks for signup mode
*
* @return string Error message label on validation error
*/
protected function validateEmail($email, $signup = false)
{
$v = Validator::make(['email' => $email], ['email' => 'required|email']);
if ($v->fails()) {
return 'validation.emailinvalid';
}
list($local, $domain) = explode('@', $email);
// don't allow @localhost and other no-fqdn
if (strpos($domain, '.') === false) {
return 'validation.emailinvalid';
}
// Extended checks for an address that is supposed to become a login to Kolab
if ($signup) {
// Local part validation
if (!preg_match('/^[A-Za-z0-9_.-]+$/', $local)) {
return 'validation.emailinvalid';
}
// Check if specified domain is allowed for signup
if ($domain != \config('app.domain')) {
return 'validation.emailinvalid';
}
// Check if the local part is not one of exceptions
$exceptions = '/^(admin|administrator|sales|root)$/i';
if (preg_match($exceptions, $local)) {
return 'validation.emailexists';
}
// Check if user with specified login already exists
// TODO: Aliases
if (User::where('email', $email)->first()) {
return 'validation.emailexists';
}
}
}
}
diff --git a/src/app/Http/Controllers/API/UsersController.php b/src/app/Http/Controllers/API/UsersController.php
index d95cd564..1f2c91b5 100644
--- a/src/app/Http/Controllers/API/UsersController.php
+++ b/src/app/Http/Controllers/API/UsersController.php
@@ -1,163 +1,182 @@
middleware('auth:api', ['except' => ['login']]);
}
+ /**
+ * Helper method for other controllers with user auto-logon
+ * functionality
+ *
+ * @param \App\User $user User model object
+ */
+ public static function logonResponse(User $user)
+ {
+ $token = auth()->login($user);
+
+ return response()->json([
+ 'status' => 'success',
+ 'access_token' => $token,
+ 'token_type' => 'bearer',
+ 'expires_in' => Auth::guard()->factory()->getTTL() * 60,
+ ]);
+ }
+
/**
* Display a listing of the resources.
*
* The user themself, and other user entitlements.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$user = Auth::user();
if (!$user) {
return response()->json(['error' => 'unauthorized'], 401);
}
$result = [$user];
$user->entitlements()->each(
function ($entitlement) {
$result[] = User::find($entitlement->user_id);
}
);
return response()->json($result);
}
/**
* Get the authenticated User
*
* @return \Illuminate\Http\JsonResponse
*/
public function info()
{
return response()->json($this->guard()->user());
}
/**
* Get a JWT token via given credentials.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
if ($token = $this->guard()->attempt($credentials)) {
return $this->respondWithToken($token);
}
return response()->json(['error' => 'Unauthorized'], 401);
}
/**
* Log the user out (Invalidate the token)
*
* @return \Illuminate\Http\JsonResponse
*/
public function logout()
{
$this->guard()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh a token.
*
* @return \Illuminate\Http\JsonResponse
*/
public function refresh()
{
return $this->respondWithToken($this->guard()->refresh());
}
/**
* Get the token array structure.
*
* @param string $token Respond with this token.
*
* @return \Illuminate\Http\JsonResponse
*/
protected function respondWithToken($token)
{
return response()->json(
[
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => $this->guard()->factory()->getTTL() * 60
]
);
}
/**
* Display the specified resource.
*
* @param int $id The account to show information for.
*
* @return \Illuminate\Http\Response
*/
public function show($id)
{
$user = Auth::user();
if (!$user) {
return abort(403);
}
$result = false;
$user->entitlements()->each(
function ($entitlement) {
if ($entitlement->user_id == $id) {
$result = true;
}
}
);
if ($user->id == $id) {
$result = true;
}
if (!$result) {
return abort(404);
}
return \App\User::find($id);
}
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\Guard
*/
public function guard()
{
return Auth::guard();
}
}
diff --git a/src/app/Http/Controllers/API/WalletsController.php b/src/app/Http/Controllers/API/WalletsController.php
index a6d4d026..94247981 100644
--- a/src/app/Http/Controllers/API/WalletsController.php
+++ b/src/app/Http/Controllers/API/WalletsController.php
@@ -1,95 +1,97 @@
code = $code;
}
/**
* Determine the time at which the job should timeout.
*
* @return \DateTime
*/
public function retryUntil()
{
// FIXME: I think it does not make sense to continue trying after 1 hour
return now()->addHours(1);
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
- Mail::to($this->code->data['email'])->send(new SignupVerification($this->code));
+ $email = $this->code->user->getSetting('external_email');
+
+ Mail::to($email)->send(new PasswordReset($this->code));
}
}
diff --git a/src/app/Jobs/ProcessUserCreate.php b/src/app/Jobs/ProcessUserCreate.php
index a04e3c36..dc3047c6 100644
--- a/src/app/Jobs/ProcessUserCreate.php
+++ b/src/app/Jobs/ProcessUserCreate.php
@@ -1,36 +1,39 @@
user = $user;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// TODO: Create the user in LDAP
}
}
diff --git a/src/app/Jobs/ProcessUserDelete.php b/src/app/Jobs/ProcessUserDelete.php
index 2bf6d58f..5a0d10a5 100644
--- a/src/app/Jobs/ProcessUserDelete.php
+++ b/src/app/Jobs/ProcessUserDelete.php
@@ -1,36 +1,39 @@
user = $user;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//
}
}
diff --git a/src/app/Jobs/ProcessUserRead.php b/src/app/Jobs/ProcessUserRead.php
index 9df8d654..55d0d981 100644
--- a/src/app/Jobs/ProcessUserRead.php
+++ b/src/app/Jobs/ProcessUserRead.php
@@ -1,36 +1,39 @@
user = $user;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//
}
}
diff --git a/src/app/Jobs/ProcessUserUpdate.php b/src/app/Jobs/ProcessUserUpdate.php
index 9d044951..541f9f9b 100644
--- a/src/app/Jobs/ProcessUserUpdate.php
+++ b/src/app/Jobs/ProcessUserUpdate.php
@@ -1,36 +1,39 @@
user = $user;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//
}
}
diff --git a/src/app/Jobs/SignupVerificationEmail.php b/src/app/Jobs/SignupVerificationEmail.php
index 63e8c958..d9812eb0 100644
--- a/src/app/Jobs/SignupVerificationEmail.php
+++ b/src/app/Jobs/SignupVerificationEmail.php
@@ -1,58 +1,60 @@
code = $code;
}
/**
* Determine the time at which the job should timeout.
*
* @return \DateTime
*/
public function retryUntil()
{
// FIXME: I think it does not make sense to continue trying after 1 hour
return now()->addHours(1);
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Mail::to($this->code->data['email'])->send(new SignupVerification($this->code));
}
}
diff --git a/src/app/Jobs/SignupVerificationSMS.php b/src/app/Jobs/SignupVerificationSMS.php
index 6ebd794c..a4189faf 100644
--- a/src/app/Jobs/SignupVerificationSMS.php
+++ b/src/app/Jobs/SignupVerificationSMS.php
@@ -1,56 +1,58 @@
code = $code;
}
/**
* Determine the time at which the job should timeout.
*
* @return \DateTime
*/
public function retryUntil()
{
// FIXME: I think it does not make sense to continue trying after 1 hour
return now()->addHours(1);
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// TODO
}
}
diff --git a/src/app/Mail/PasswordReset.php b/src/app/Mail/PasswordReset.php
new file mode 100644
index 00000000..97777b8d
--- /dev/null
+++ b/src/app/Mail/PasswordReset.php
@@ -0,0 +1,57 @@
+code = $code;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $href = sprintf(
+ '%s/login/reset/%s-%s',
+ \config('app.url'),
+ $this->code->short_code,
+ $this->code->code
+ );
+
+ $this->view('emails.password_reset')
+ ->subject(__('mail.passwordreset-subject', ['site' => \config('app.name')]))
+ ->with([
+ 'site' => \config('app.name'),
+ 'code' => $this->code->code,
+ 'short_code' => $this->code->short_code,
+ 'link' => sprintf('%s', $href, $href),
+ 'username' => $this->code->user->name
+ ]);
+
+ return $this;
+ }
+}
diff --git a/src/app/Mail/SignupVerification.php b/src/app/Mail/SignupVerification.php
index 9fc88881..78481671 100644
--- a/src/app/Mail/SignupVerification.php
+++ b/src/app/Mail/SignupVerification.php
@@ -1,50 +1,57 @@
code = $code;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
+ $href = sprintf(
+ '%s/signup/%s-%s',
+ \config('app.url'),
+ $this->code->short_code,
+ $this->code->code
+ );
+
$this->view('emails.signup_code')
->subject(__('mail.signupcode-subject', ['site' => \config('app.name')]))
->with([
'site' => \config('app.name'),
'username' => $this->code->data['name'],
'code' => $this->code->code,
'short_code' => $this->code->short_code,
- 'url_code' => $this->code->short_code . '-' . $this->code->code,
+ 'link' => sprintf('%s', $href, $href),
]);
return $this;
}
}
diff --git a/src/app/Observers/SignupCodeObserver.php b/src/app/Observers/SignupCodeObserver.php
index 7222059b..45463df9 100644
--- a/src/app/Observers/SignupCodeObserver.php
+++ b/src/app/Observers/SignupCodeObserver.php
@@ -1,38 +1,38 @@
code)) {
$code->short_code = SignupCode::generateShortCode();
// FIXME: Replace this with something race-condition free
while (true) {
$code->code = str_random($code_length);
if (!SignupCode::find($code->code)) {
break;
}
}
}
$code->expires_at = Carbon::now()->addHours($exp_hours);
}
}
diff --git a/src/app/Observers/SignupCodeObserver.php b/src/app/Observers/VerificationCodeObserver.php
similarity index 56%
copy from src/app/Observers/SignupCodeObserver.php
copy to src/app/Observers/VerificationCodeObserver.php
index 7222059b..9b41bc66 100644
--- a/src/app/Observers/SignupCodeObserver.php
+++ b/src/app/Observers/VerificationCodeObserver.php
@@ -1,38 +1,39 @@
code)) {
- $code->short_code = SignupCode::generateShortCode();
+ $code->short_code = VerificationCode::generateShortCode();
// FIXME: Replace this with something race-condition free
while (true) {
$code->code = str_random($code_length);
- if (!SignupCode::find($code->code)) {
+ if (!VerificationCode::find($code->code)) {
break;
}
}
}
$code->expires_at = Carbon::now()->addHours($exp_hours);
}
}
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
index d949d51f..3934520f 100644
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -1,42 +1,43 @@
sql, implode(', ', $query->bindings)));
});
}
}
}
diff --git a/src/app/SignupCode.php b/src/app/SignupCode.php
index 59b56ec5..95417f1b 100644
--- a/src/app/SignupCode.php
+++ b/src/app/SignupCode.php
@@ -1,100 +1,100 @@
'array'];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['expires_at'];
/**
* Check if code is expired.
*
* @return bool True if code is expired, False otherwise
*/
public function isExpired()
{
return $this->expires_at ? Carbon::now()->gte($this->expires_at) : false;
}
/**
* Generate a short code (for human).
*
* @return string
*/
public static function generateShortCode(): string
{
$code_length = env('SIGNUP_CODE_LENGTH', self::SHORTCODE_LENGTH);
$code_chars = env('SIGNUP_CODE_CHARS', self::SHORTCODE_CHARS);
$random = [];
for ($i = 1; $i <= $code_length; $i++) {
$random[] = $code_chars[rand(0, strlen($code_chars) - 1)];
}
shuffle($random);
return implode('', $random);
}
}
diff --git a/src/app/Traits/UserSettingsTrait.php b/src/app/Traits/UserSettingsTrait.php
index 842050f8..f9430d65 100644
--- a/src/app/Traits/UserSettingsTrait.php
+++ b/src/app/Traits/UserSettingsTrait.php
@@ -1,110 +1,113 @@
'some@other.erg']);
* $locale = $user->getSetting('locale');
* ```
*
* @param string $key Lookup key
*
* @return string
*/
- public function getSetting($key)
+ public function getSetting(string $key)
{
$settings = $this->getCache();
$value = array_get($settings, $key);
return ($value !== '') ? $value : null;
}
/**
* Create or update a setting.
*
* Example Usage:
*
* ```php
* $user = User::firstOrCreate(['email' => 'some@other.erg']);
* $user->setSetting('locale', 'en');
* ```
*
* @param string $key Setting name
* @param string $value The new value for the setting.
*
* @return void
*/
- public function setSetting($key, $value)
+ public function setSetting(string $key, $value)
{
$this->storeSetting($key, $value);
$this->setCache();
}
/**
* Create or update multiple settings in one fell swoop.
*
* Example Usage:
*
* ```php
* $user = User::firstOrCreate(['email' => 'some@other.erg']);
* $user->setSettings(['locale', 'en', 'country' => 'GB']);
* ```
*
* @param array $data An associative array of key value pairs.
*
* @return void
*/
- public function setSettings($data = [])
+ public function setSettings(array $data = [])
{
foreach ($data as $key => $value) {
$this->storeSetting($key, $value);
}
$this->setCache();
}
- private function storeSetting($key, $value)
+ private function storeSetting(string $key, $value)
{
$record = UserSetting::where(['user_id' => $this->id, 'key' => $key])->first();
if ($record) {
$record->value = $value;
$record->save();
} else {
$data = new UserSetting(['key' => $key, 'value' => $value]);
$this->settings()->save($data);
}
}
private function getCache()
{
if (Cache::has('user_settings_' . $this->id)) {
return Cache::get('user_settings_' . $this->id);
}
return $this->setCache();
}
private function setCache()
{
if (Cache::has('user_settings_' . $this->id)) {
Cache::forget('user_settings_' . $this->id);
}
- $settings = $this->settings()->get();
+ $cached = [];
+ foreach ($this->settings()->get() as $entry) {
+ $cached[$entry->key] = $entry->value;
+ }
- Cache::forever('user_settings_' . $this->id, $settings);
+ Cache::forever('user_settings_' . $this->id, $cached);
return $this->getCache();
}
}
diff --git a/src/app/User.php b/src/app/User.php
index 010c8b38..4a2d8bcc 100644
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -1,190 +1,206 @@
'datetime',
];
/**
* Any wallets on which this user is a controller.
*
* @return Wallet[]
*/
public function accounts()
{
return $this->belongsToMany(
'App\Wallet', // The foreign object definition
'user_accounts', // The table name
'user_id', // The local foreign key
'wallet_id' // The remote foreign key
);
}
/**
* List the domains to which this user is entitled.
*
* @return Domain[]
*/
public function domains()
{
$domains = Domain::whereRaw(
sprintf(
'(type & %s) AND (status & %s)',
Domain::TYPE_PUBLIC,
Domain::STATUS_ACTIVE
)
)->get();
foreach ($this->entitlements()->get() as $entitlement) {
if ($entitlement->entitleable instanceof Domain) {
$domain = Domain::find($entitlement->entitleable_id);
\Log::info("Found domain {$domain->namespace}");
$domains[] = $domain;
}
}
foreach ($this->accounts()->get() as $wallet) {
foreach ($wallet->entitlements()->get() as $entitlement) {
if ($entitlement->entitleable instanceof Domain) {
$domain = Domain::find($entitlement->entitleable_id);
\Log::info("Found domain {$domain->namespace}");
$domains[] = $domain;
}
}
}
return $domains;
}
public function entitlement()
{
return $this->morphOne('App\Entitlement', 'entitleable');
}
/**
* Entitlements for this user.
*
* @return Entitlement[]
*/
public function entitlements()
{
return $this->hasMany('App\Entitlement', 'owner_id', 'id');
}
public function addEntitlement($entitlement)
{
if (!$this->entitlements()->get()->contains($entitlement)) {
return $this->entitlements()->save($entitlement);
}
}
+ /**
+ * Helper to find user by email address, whether it is
+ * main email address, alias or external email
+ *
+ * @param string $email Email address
+ *
+ * @return \App\User User model object
+ */
+ public static function findByEmail(string $email)
+ {
+ if (strpos($email, '@') === false) {
+ return;
+ }
+
+ $user = self::where('email', $email);
+
+ return $user->count() === 1 ? $user->first() : null;
+
+ // TODO: Aliases, External email
+ }
+
public function settings()
{
return $this->hasMany('App\UserSetting', 'user_id');
}
/**
- * Return single user setting value
- *
- * @param string $key Setting key name
+ * Verification codes for this user.
*
- * @return string Setting value
+ * @return VerificationCode[]
*/
- public function getSetting(string $key, $default = null)
+ public function verificationcodes()
{
- $setting = $this->settings->where('key', $key)->first();
-
- return $setting ? $setting->value : $default;
+ return $this->hasMany('App\VerificationCode', 'user_id', 'id');
}
/**
* Wallets this user owns.
*
* @return Wallet[]
*/
public function wallets()
{
return $this->hasMany('App\Wallet');
}
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
public function setPasswordAttribute($password)
{
if (!empty($password)) {
$this->attributes['password'] = bcrypt($password, [ "rounds" => 12 ]);
$this->attributes['password_ldap'] = '{SSHA512}' . base64_encode(
pack('H*', hash('sha512', $password))
);
}
}
public function setPasswordLdapAttribute($password)
{
if (!empty($password)) {
$this->attributes['password'] = bcrypt($password, [ "rounds" => 12 ]);
$this->attributes['password_ldap'] = '{SSHA512}' . base64_encode(
pack('H*', hash('sha512', $password))
);
}
}
}
diff --git a/src/app/UserSetting.php b/src/app/UserSetting.php
index 3cf0ad0d..e2908972 100644
--- a/src/app/UserSetting.php
+++ b/src/app/UserSetting.php
@@ -1,30 +1,30 @@
belongsTo('\App\User', 'id', 'user_id');
+ return $this->belongsTo('\App\User', 'user_id', 'id');
}
}
diff --git a/src/app/VerificationCode.php b/src/app/VerificationCode.php
new file mode 100644
index 00000000..80e4b7c7
--- /dev/null
+++ b/src/app/VerificationCode.php
@@ -0,0 +1,62 @@
+belongsTo('\App\User', 'user_id', 'id');
+ }
+
+ /**
+ * Generate a short code (for human).
+ *
+ * @return string
+ */
+ public static function generateShortCode(): string
+ {
+ $code_length = env('VERIFICATION_CODE_LENGTH', self::SHORTCODE_LENGTH);
+ $code_chars = env('VERIFICATION_CODE_CHARS', self::SHORTCODE_CHARS);
+ $random = [];
+
+ for ($i = 1; $i <= $code_length; $i++) {
+ $random[] = $code_chars[rand(0, strlen($code_chars) - 1)];
+ }
+
+ shuffle($random);
+
+ return implode('', $random);
+ }
+}
diff --git a/src/database/migrations/2019_12_20_130000_create_verification_codes_table.php b/src/database/migrations/2019_12_20_130000_create_verification_codes_table.php
new file mode 100644
index 00000000..e8daf8c6
--- /dev/null
+++ b/src/database/migrations/2019_12_20_130000_create_verification_codes_table.php
@@ -0,0 +1,37 @@
+bigInteger('user_id');
+ $table->string('code', 32);
+ $table->string('short_code', 16);
+ $table->string('mode');
+ $table->timestamp('expires_at');
+
+ $table->primary('code');
+
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::drop('verification_codes');
+ }
+}
diff --git a/src/resources/lang/en/mail.php b/src/resources/lang/en/mail.php
index d1f08499..5b5b3f97 100644
--- a/src/resources/lang/en/mail.php
+++ b/src/resources/lang/en/mail.php
@@ -1,19 +1,25 @@
"Dear :name,",
'footer' => "Best regards,\nYour :site Team",
+ 'passwordreset-subject' => ":site Password Reset",
+ 'passwordreset-body' => "Someone recently asked to change your :site password.\n"
+ . "If this was you, use this verification code to complete the process: :code.\n"
+ . "You can also click the link below.\n"
+ . "If you did not make such a request, you can either ignore this message or get in touch with us about this incident.",
+
'signupcode-subject' => ":site Registration",
'signupcode-body' => "This is your verification code for the :site registration process: :code.\n"
. "You can also click the link below to continue the registration process:",
];
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
index 57695f52..23261b79 100644
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -1,154 +1,156 @@
'The :attribute must be accepted.',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'before' => 'The :attribute must be a date before :date.',
'before_or_equal' => 'The :attribute must be a date before or equal to :date.',
'between' => [
'numeric' => 'The :attribute must be between :min and :max.',
'file' => 'The :attribute must be between :min and :max kilobytes.',
'string' => 'The :attribute must be between :min and :max characters.',
'array' => 'The :attribute must have between :min and :max items.',
],
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_equals' => 'The :attribute must be a date equal to :date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'email' => 'The :attribute must be a valid email address.',
'ends_with' => 'The :attribute must end with one of the following: :values',
'exists' => 'The selected :attribute is invalid.',
'file' => 'The :attribute must be a file.',
'filled' => 'The :attribute field must have a value.',
'gt' => [
'numeric' => 'The :attribute must be greater than :value.',
'file' => 'The :attribute must be greater than :value kilobytes.',
'string' => 'The :attribute must be greater than :value characters.',
'array' => 'The :attribute must have more than :value items.',
],
'gte' => [
'numeric' => 'The :attribute must be greater than or equal :value.',
'file' => 'The :attribute must be greater than or equal :value kilobytes.',
'string' => 'The :attribute must be greater than or equal :value characters.',
'array' => 'The :attribute must have :value items or more.',
],
'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.',
'in_array' => 'The :attribute field does not exist in :other.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'ipv4' => 'The :attribute must be a valid IPv4 address.',
'ipv6' => 'The :attribute must be a valid IPv6 address.',
'json' => 'The :attribute must be a valid JSON string.',
'lt' => [
'numeric' => 'The :attribute must be less than :value.',
'file' => 'The :attribute must be less than :value kilobytes.',
'string' => 'The :attribute must be less than :value characters.',
'array' => 'The :attribute must have less than :value items.',
],
'lte' => [
'numeric' => 'The :attribute must be less than or equal :value.',
'file' => 'The :attribute must be less than or equal :value kilobytes.',
'string' => 'The :attribute must be less than or equal :value characters.',
'array' => 'The :attribute must not have more than :value items.',
],
'max' => [
'numeric' => 'The :attribute may not be greater than :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.',
'string' => 'The :attribute may not be greater than :max characters.',
'array' => 'The :attribute may not have more than :max items.',
],
'mimes' => 'The :attribute must be a file of type: :values.',
'mimetypes' => 'The :attribute must be a file of type: :values.',
'min' => [
'numeric' => 'The :attribute must be at least :min.',
'file' => 'The :attribute must be at least :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.',
'array' => 'The :attribute must have at least :min items.',
],
'not_in' => 'The selected :attribute is invalid.',
'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'present' => 'The :attribute field must be present.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values are present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
'size' => [
'numeric' => 'The :attribute must be :size.',
'file' => 'The :attribute must be :size kilobytes.',
'string' => 'The :attribute must be :size characters.',
'array' => 'The :attribute must contain :size items.',
],
'starts_with' => 'The :attribute must start with one of the following: :values',
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'url' => 'The :attribute format is invalid.',
'uuid' => 'The :attribute must be a valid UUID.',
'emailexists' => 'The specified email address already exists',
'emailinvalid' => 'The specified email address is invalid',
'noemailorphone' => 'The specified text is neither a valid email address nor a phone number',
+ 'usernotexists' => 'Unable to find user',
+ 'noextemail' => 'This user has no external email address',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];
diff --git a/src/resources/views/emails/signup_code.blade.php b/src/resources/views/emails/password_reset.blade.php
similarity index 59%
copy from src/resources/views/emails/signup_code.blade.php
copy to src/resources/views/emails/password_reset.blade.php
index d5f41c63..85751758 100644
--- a/src/resources/views/emails/signup_code.blade.php
+++ b/src/resources/views/emails/password_reset.blade.php
@@ -1,15 +1,15 @@
{{ __('mail.header', ['name' => $username]) }}
- {{ __('mail.signupcode-body', ['code' => $short_code, 'site' => $site]) }}
+ {{ __('mail.passwordreset-body', ['code' => $short_code, 'site' => $site]) }}
- {{ config('app.url') }}/signup/{{ $url_code }}
+ {!! $link !!}
{{ __('mail.footer', ['site' => $site, 'appurl' => config('app.url')]) }}
diff --git a/src/resources/views/emails/signup_code.blade.php b/src/resources/views/emails/signup_code.blade.php
index d5f41c63..e14af11a 100644
--- a/src/resources/views/emails/signup_code.blade.php
+++ b/src/resources/views/emails/signup_code.blade.php
@@ -1,15 +1,15 @@
{{ __('mail.header', ['name' => $username]) }}
{{ __('mail.signupcode-body', ['code' => $short_code, 'site' => $site]) }}
- {{ config('app.url') }}/signup/{{ $url_code }}
+ {!! $link !!}
{{ __('mail.footer', ['site' => $site, 'appurl' => config('app.url')]) }}
diff --git a/src/resources/vue/components/Login.vue b/src/resources/vue/components/Login.vue
index 6427c23f..358dd24e 100644
--- a/src/resources/vue/components/Login.vue
+++ b/src/resources/vue/components/Login.vue
@@ -1,79 +1,81 @@
diff --git a/src/resources/vue/components/Signup.vue b/src/resources/vue/components/PasswordReset.vue
similarity index 65%
copy from src/resources/vue/components/Signup.vue
copy to src/resources/vue/components/PasswordReset.vue
index 5be756f5..156c079e 100644
--- a/src/resources/vue/components/Signup.vue
+++ b/src/resources/vue/components/PasswordReset.vue
@@ -1,184 +1,161 @@
-
Step 1/3
+
Password Reset - Step 1/3
- Sign up to start your free month.
+ Enter your email address to reset your password. You may need to check your spam folder or unblock noreply@kolabnow.com.
-
-
Step 2/3
+
Password Reset - Step 2/3
- We sent out a confirmation code to your email address.
+ We sent out a confirmation code to your external email address.
Enter the code we sent you, or click the link in the message.
-
-
Step 3/3
+
Password Reset - Step 3/3
- Create your Kolab identity (you can choose additional addresses later).
-
-
diff --git a/src/resources/vue/components/Signup.vue b/src/resources/vue/components/Signup.vue
index 5be756f5..1aba7e8c 100644
--- a/src/resources/vue/components/Signup.vue
+++ b/src/resources/vue/components/Signup.vue
@@ -1,184 +1,183 @@
-
Step 1/3
+
Sign Up - Step 1/3
Sign up to start your free month.
-
Step 2/3
+
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.
-
Step 3/3
+
Sign Up - Step 3/3
Create your Kolab identity (you can choose additional addresses later).
-
diff --git a/src/resources/vue/js/routes.js b/src/resources/vue/js/routes.js
index 2e4373a0..edbd1805 100644
--- a/src/resources/vue/js/routes.js
+++ b/src/resources/vue/js/routes.js
@@ -1,70 +1,76 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import DashboardComponent from '../components/Dashboard'
import Error404Component from '../components/404'
import LoginComponent from '../components/Login'
import LogoutComponent from '../components/Logout'
+import PasswordResetComponent from '../components/PasswordReset'
import SignupComponent from '../components/Signup'
import store from './store'
const routes = [
{
path: '/',
redirect: { name: 'login' }
},
{
path: '/dashboard',
name: 'dashboard',
component: DashboardComponent,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'login',
component: LoginComponent
},
{
path: '/logout',
name: 'logout',
component: LogoutComponent
},
+ {
+ path: '/password-reset/:code?',
+ name: 'password-reset',
+ component: PasswordResetComponent
+ },
{
path: '/signup/:code?',
name: 'signup',
component: SignupComponent
},
{
name: '404',
path: '*',
component: Error404Component
}
]
const router = new VueRouter({
mode: 'history',
routes
})
router.beforeEach((to, from, next) => {
// check if the route requires authentication and user is not logged in
if (to.matched.some(route => route.meta.requiresAuth) && !store.state.isLoggedIn) {
// redirect to login page
next({ name: 'login' })
return
}
// if logged in redirect to dashboard
if (to.path === '/login' && store.state.isLoggedIn) {
next({ name: 'dashboard' })
return
}
next()
})
export default router
diff --git a/src/routes/api.php b/src/routes/api.php
index 76d8cef6..c4906bb9 100644
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -1,43 +1,47 @@
'api',
'prefix' => 'auth'
],
function ($router) {
Route::get('info', 'API\UsersController@info');
Route::post('login', 'API\UsersController@login');
Route::post('logout', 'API\UsersController@logout');
Route::post('refresh', 'API\UsersController@refresh');
+ Route::post('password-reset/init', 'API\PasswordResetController@init');
+ Route::post('password-reset/verify', 'API\PasswordResetController@verify');
+ Route::post('password-reset', 'API\PasswordResetController@reset');
+
Route::post('signup/init', 'API\SignupController@init');
Route::post('signup/verify', 'API\SignupController@verify');
Route::post('signup', 'API\SignupController@signup');
}
);
Route::group(
[
'middleware' => 'auth:api',
'prefix' => 'v4'
],
function () {
Route::apiResource('entitlements', API\EntitlementsController::class);
Route::apiResource('users', API\UsersController::class);
Route::apiResource('wallets', API\WalletsController::class);
}
);
diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php
index 78be7c57..c21601ef 100644
--- a/src/tests/Browser/LogonTest.php
+++ b/src/tests/Browser/LogonTest.php
@@ -1,64 +1,63 @@
browse(function (Browser $browser) {
$browser->visit('/dashboard');
// Checks if we're really on the login page
$browser->waitForLocation('/login')
->on(new Home());
});
}
/**
* Logon with wrong password/user test
*
* @return void
*/
public function testLogonWrongCredentials()
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'wrong');
// Checks if we're still on the logon page
// FIXME: This assertion might be prone to timing issues
// I guess we should wait until some error message appears
$browser->on(new Home());
});
}
/**
* Successful logon test
*
* @return void
*/
public function testLogonSuccessful()
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'simple123', true);
// Checks if we're really on Dashboard page
$browser->on(new Dashboard());
});
}
}
diff --git a/src/tests/Browser/Pages/PasswordReset.php b/src/tests/Browser/Pages/PasswordReset.php
new file mode 100644
index 00000000..1b9429e7
--- /dev/null
+++ b/src/tests/Browser/Pages/PasswordReset.php
@@ -0,0 +1,47 @@
+assertPathIs('/password-reset');
+ $browser->assertPresent('@step1');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@step1' => '#step1',
+ '@step2' => '#step2',
+ '@step3' => '#step3',
+ ];
+ }
+}
diff --git a/src/tests/Browser/PasswordResetTest.php b/src/tests/Browser/PasswordResetTest.php
new file mode 100644
index 00000000..753daad8
--- /dev/null
+++ b/src/tests/Browser/PasswordResetTest.php
@@ -0,0 +1,258 @@
+ 'passwordresettestdusk@' . \config('app.domain')]);
+ $user->setSetting('external_email', 'external@domain.tld');
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return void
+ */
+ public function tearDown(): void
+ {
+ User::where('email', 'passwordresettestdusk@' . \config('app.domain'))->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test 1st step of password-reset
+ *
+ * @return void
+ */
+ public function testPasswordResetStep1()
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new PasswordReset());
+
+ $browser->assertVisible('@step1');
+
+ // Here we expect email input and submit button
+ $browser->with('@step1', function ($step) {
+ $step->assertVisible('#reset_email');
+ $step->assertFocused('#reset_email');
+ $step->assertVisible('[type=submit]');
+ });
+
+ // Submit empty form
+ $browser->with('@step1', function ($step) {
+ $step->click('[type=submit]');
+ $step->assertFocused('#reset_email');
+ });
+
+ // Submit invalid email
+ // We expect email input to have is-invalid class added, with .invalid-feedback element
+ $browser->with('@step1', function ($step) use ($browser) {
+ $step->type('#reset_email', '@test');
+ $step->click('[type=submit]');
+
+ $step->waitFor('#reset_email.is-invalid');
+ $step->waitFor('#reset_email + .invalid-feedback');
+ $browser->waitFor('.toast-error');
+ $browser->click('.toast-error'); // remove the toast
+ });
+
+ // Submit valid data
+ $browser->with('@step1', function ($step) {
+ $step->type('#reset_email', 'passwordresettestdusk@' . \config('app.domain'));
+ $step->click('[type=submit]');
+
+ $step->assertMissing('#reset_email.is-invalid');
+ $step->assertMissing('#reset_email + .invalid-feedback');
+ });
+
+ $browser->waitUntilMissing('@step2 #reset_code[value=""]');
+ $browser->waitFor('@step2');
+ $browser->assertMissing('@step1');
+ });
+ }
+
+ /**
+ * Test 2nd Step of the password reset process
+ *
+ * @depends testPasswordResetStep1
+ * @return void
+ */
+ public function testPasswordResetStep2()
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->assertVisible('@step2');
+
+ // Here we expect one text input, Back and Continue buttons
+ $browser->with('@step2', function ($step) {
+ $step->assertVisible('#reset_short_code');
+ $step->assertFocused('#reset_short_code');
+ $step->assertVisible('[type=button]');
+ $step->assertVisible('[type=submit]');
+ });
+
+ // Test Back button functionality
+ $browser->click('@step2 [type=button]');
+ $browser->waitFor('@step1');
+ $browser->assertFocused('@step1 #reset_email');
+ $browser->assertMissing('@step2');
+
+ // Submit valid Step 1 data (again)
+ $browser->with('@step1', function ($step) {
+ $step->type('#reset_email', 'passwordresettestdusk@' . \config('app.domain'));
+ $step->click('[type=submit]');
+ });
+
+ $browser->waitFor('@step2');
+ $browser->assertMissing('@step1');
+
+ // Submit invalid code
+ // We expect code input to have is-invalid class added, with .invalid-feedback element
+ $browser->with('@step2', function ($step) use ($browser) {
+ $step->type('#reset_short_code', 'XXXXX');
+ $step->click('[type=submit]');
+
+ $browser->waitFor('.toast-error');
+
+ $step->assertVisible('#reset_short_code.is-invalid');
+ $step->assertVisible('#reset_short_code + .invalid-feedback');
+ $step->assertFocused('#reset_short_code');
+
+ $browser->click('.toast-error'); // remove the toast
+ });
+
+ // Submit valid code
+ // We expect error state on code input to be removed, and Step 3 form visible
+ $browser->with('@step2', function ($step) {
+ // Get the code and short_code from database
+ // FIXME: Find a nice way to read javascript data without using hidden inputs
+ $code = $step->value('#reset_code');
+
+ $this->assertNotEmpty($code);
+
+ $code = VerificationCode::find($code);
+
+ $step->type('#reset_short_code', $code->short_code);
+ $step->click('[type=submit]');
+
+ $step->assertMissing('#reset_short_code.is-invalid');
+ $step->assertMissing('#reset_short_code + .invalid-feedback');
+ });
+
+ $browser->waitFor('@step3');
+ $browser->assertMissing('@step2');
+ });
+ }
+
+ /**
+ * Test 3rd Step of the password reset process
+ *
+ * @depends testPasswordResetStep2
+ * @return void
+ */
+ public function testPasswordResetStep3()
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->assertVisible('@step3');
+
+ // Here we expect 2 text inputs, Back and Continue buttons
+ $browser->with('@step3', function ($step) {
+ $step->assertVisible('#reset_password');
+ $step->assertVisible('#reset_confirm');
+ $step->assertVisible('[type=button]');
+ $step->assertVisible('[type=submit]');
+ $step->assertFocused('#reset_password');
+ });
+
+ // Test Back button
+ $browser->click('@step3 [type=button]');
+ $browser->waitFor('@step2');
+ $browser->assertFocused('@step2 #reset_short_code');
+ $browser->assertMissing('@step3');
+ $browser->assertMissing('@step1');
+
+ // TODO: Test form reset when going back
+
+ // Because the verification code is removed in tearDown()
+ // we'll start from the beginning (Step 1)
+ $browser->click('@step2 [type=button]');
+ $browser->waitFor('@step1');
+ $browser->assertFocused('@step1 #reset_email');
+ $browser->assertMissing('@step3');
+ $browser->assertMissing('@step2');
+
+ // Submit valid data
+ $browser->with('@step1', function ($step) {
+ $step->type('#reset_email', 'passwordresettestdusk@' . \config('app.domain'));
+ $step->click('[type=submit]');
+ });
+
+ $browser->waitFor('@step2');
+ $browser->waitUntilMissing('@step2 #reset_code[value=""]');
+
+ // Submit valid code again
+ $browser->with('@step2', function ($step) {
+ $code = $step->value('#reset_code');
+
+ $this->assertNotEmpty($code);
+
+ $code = VerificationCode::find($code);
+
+ $step->type('#reset_short_code', $code->short_code);
+ $step->click('[type=submit]');
+ });
+
+ $browser->waitFor('@step3');
+
+ // Submit invalid data
+ $browser->with('@step3', function ($step) use ($browser) {
+ $step->assertFocused('#reset_password');
+
+ $step->type('#reset_password', '12345678');
+ $step->type('#reset_confirm', '123456789');
+
+ $step->click('[type=submit]');
+
+ $browser->waitFor('.toast-error');
+
+ $step->assertVisible('#reset_password.is-invalid');
+ $step->assertVisible('#reset_password + .invalid-feedback');
+ $step->assertFocused('#reset_password');
+
+ $browser->click('.toast-error'); // remove the toast
+ });
+
+ // Submit valid data
+ $browser->with('@step3', function ($step) {
+ $step->type('#reset_confirm', '12345678');
+
+ $step->click('[type=submit]');
+ });
+
+ $browser->waitUntilMissing('@step3');
+
+ // At this point we should be auto-logged-in to dashboard
+ $dashboard = new Dashboard();
+ $dashboard->assert($browser);
+
+ // FIXME: Is it enough to be sure user is logged in?
+ });
+ }
+}
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
index 5fe5382d..9521692d 100644
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -1,320 +1,313 @@
delete();
parent::tearDown();
}
/**
* Test signup code verification with a link
*
* @return void
*/
public function testSignupCodeByLink()
{
// Test invalid code (invalid format)
$this->browse(function (Browser $browser) {
// Register Signup page element selectors we'll be using
$browser->onWithoutAssert(new Signup());
// TODO: Test what happens if user is logged in
$browser->visit('/signup/invalid-code');
// TODO: According to https://github.com/vuejs/vue-router/issues/977
// it is not yet easily possible to display error page component (route)
// without changing the URL
// TODO: Instead of css selector we should probably define page/component
// and use it instead
$browser->waitFor('#error-page');
});
// Test invalid code (valid format)
$this->browse(function (Browser $browser) {
$browser->visit('/signup/XXXXX-code');
// FIXME: User will not be able to continue anyway, so we should
// either display 1st step or 404 error page
$browser->waitFor('@step1');
$browser->waitFor('.toast-error');
$browser->click('.toast-error'); // remove the toast
});
// Test valid code
$this->browse(function (Browser $browser) {
$code = SignupCode::create([
'data' => [
'email' => 'User@example.org',
'name' => 'User Name',
]
]);
$browser->visit('/signup/' . $code->short_code . '-' . $code->code);
$browser->waitFor('@step3');
$browser->assertMissing('@step1');
$browser->assertMissing('@step2');
// FIXME: Find a nice way to read javascript data without using hidden inputs
$this->assertSame($code->code, $browser->value('@step2 #signup_code'));
// TODO: Test if the signup process can be completed
});
}
/**
* Test 1st step of the signup process
*
* @return void
*/
public function testSignupStep1()
{
$this->browse(function (Browser $browser) {
$browser->visit(new Signup());
$browser->assertVisible('@step1');
// Here we expect two text inputs and Continue
$browser->with('@step1', function ($step) {
$step->assertVisible('#signup_name');
$step->assertFocused('#signup_name');
$step->assertVisible('#signup_email');
$step->assertVisible('[type=submit]');
});
// Submit empty form
// Both Step 1 inputs are required, so after pressing Submit
// we expect focus to be moved to the first input
$browser->with('@step1', function ($step) {
$step->click('[type=submit]');
$step->assertFocused('#signup_name');
});
// Submit invalid email
// We expect email input to have is-invalid class added, with .invalid-feedback element
$browser->with('@step1', function ($step) use ($browser) {
$step->type('#signup_name', 'Test User');
$step->type('#signup_email', '@test');
$step->click('[type=submit]');
$step->waitFor('#signup_email.is-invalid');
$step->waitFor('#signup_email + .invalid-feedback');
$browser->waitFor('.toast-error');
$browser->click('.toast-error'); // remove the toast
});
// Submit valid data
// We expect error state on email input to be removed, and Step 2 form visible
$browser->with('@step1', function ($step) {
$step->type('#signup_name', 'Test User');
$step->type('#signup_email', 'BrowserSignupTestUser1@kolab.org');
$step->click('[type=submit]');
$step->assertMissing('#signup_email.is-invalid');
$step->assertMissing('#signup_email + .invalid-feedback');
-
- $step->waitUntilMissing('#signup_code[value=""]');
});
+ $browser->waitUntilMissing('@step2 #signup_code[value=""]');
$browser->waitFor('@step2');
$browser->assertMissing('@step1');
});
}
/**
* Test 2nd Step of the signup process
*
* @depends testSignupStep1
* @return void
*/
public function testSignupStep2()
{
$this->browse(function (Browser $browser) {
$browser->assertVisible('@step2');
// Here we expect one text input, Back and Continue buttons
$browser->with('@step2', function ($step) {
$step->assertVisible('#signup_short_code');
$step->assertFocused('#signup_short_code');
$step->assertVisible('[type=button]');
$step->assertVisible('[type=submit]');
});
// Test Back button functionality
$browser->click('@step2 [type=button]');
$browser->waitFor('@step1');
$browser->assertFocused('@step1 #signup_name');
$browser->assertMissing('@step2');
// Submit valid Step 1 data (again)
$browser->with('@step1', function ($step) {
$step->type('#signup_name', 'Test User');
$step->type('#signup_email', 'BrowserSignupTestUser1@kolab.org');
$step->click('[type=submit]');
});
$browser->waitFor('@step2');
$browser->assertMissing('@step1');
// Submit invalid code
// We expect code input to have is-invalid class added, with .invalid-feedback element
$browser->with('@step2', function ($step) use ($browser) {
$step->type('#signup_short_code', 'XXXXX');
$step->click('[type=submit]');
$browser->waitFor('.toast-error');
$step->assertVisible('#signup_short_code.is-invalid');
$step->assertVisible('#signup_short_code + .invalid-feedback');
$step->assertFocused('#signup_short_code');
$browser->click('.toast-error'); // remove the toast
});
// Submit valid code
// We expect error state on code input to be removed, and Step 3 form visible
$browser->with('@step2', function ($step) {
// Get the code and short_code from database
// FIXME: Find a nice way to read javascript data without using hidden inputs
$code = $step->value('#signup_code');
$this->assertNotEmpty($code);
$code = SignupCode::find($code);
$step->type('#signup_short_code', $code->short_code);
$step->click('[type=submit]');
$step->assertMissing('#signup_short_code.is-invalid');
$step->assertMissing('#signup_short_code + .invalid-feedback');
});
$browser->waitFor('@step3');
$browser->assertMissing('@step2');
});
}
/**
* Test 3rd Step of the signup process
*
* @depends testSignupStep2
* @return void
*/
public function testSignupStep3()
{
$this->browse(function (Browser $browser) {
$browser->assertVisible('@step3');
- // Here we expect one text input, Back and Continue buttons
+ // 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('[type=button]');
$step->assertVisible('[type=submit]');
$step->assertFocused('#signup_login');
$step->assertSeeIn('#signup_login + span', '@' . \config('app.domain'));
});
// Test Back button
$browser->click('@step3 [type=button]');
$browser->waitFor('@step2');
$browser->assertFocused('@step2 #signup_short_code');
$browser->assertMissing('@step3');
// TODO: Test form reset when going back
// Submit valid code again
$browser->with('@step2', function ($step) {
$code = $step->value('#signup_code');
$this->assertNotEmpty($code);
$code = SignupCode::find($code);
$step->type('#signup_short_code', $code->short_code);
$step->click('[type=submit]');
});
$browser->waitFor('@step3');
// Submit invalid data
$browser->with('@step3', function ($step) use ($browser) {
$step->assertFocused('#signup_login');
$step->type('#signup_login', '*');
$step->type('#signup_password', '12345678');
$step->type('#signup_confirm', '123456789');
$step->click('[type=submit]');
$browser->waitFor('.toast-error');
$step->assertVisible('#signup_login.is-invalid');
$step->assertVisible('#signup_login + span + .invalid-feedback');
$step->assertVisible('#signup_password.is-invalid');
$step->assertVisible('#signup_password + .invalid-feedback');
$step->assertFocused('#signup_login');
$browser->click('.toast-error'); // remove the toast
});
// Submit invalid data (valid login, invalid password)
$browser->with('@step3', function ($step) use ($browser) {
- // FIXME: For some reason I can't just use ->value() here
- $step->clear('#signup_login');
$step->type('#signup_login', 'SignupTestDusk');
$step->click('[type=submit]');
$browser->waitFor('.toast-error');
$step->assertVisible('#signup_password.is-invalid');
$step->assertVisible('#signup_password + .invalid-feedback');
$step->assertMissing('#signup_login.is-invalid');
$step->assertMissing('#signup_login + span + .invalid-feedback');
$step->assertFocused('#signup_password');
$browser->click('.toast-error'); // remove the toast
});
// Submit valid data
$browser->with('@step3', function ($step) {
- // FIXME: For some reason I can't just use ->value() here
- $step->clear('#signup_confirm');
$step->type('#signup_confirm', '12345678');
$step->click('[type=submit]');
});
$browser->waitUntilMissing('@step3');
// At this point we should be auto-logged-in to dashboard
$dashboard = new Dashboard();
$dashboard->assert($browser);
// FIXME: Is it enough to be sure user is logged in?
});
}
}
diff --git a/src/tests/Feature/Controller/PasswordResetTest.php b/src/tests/Feature/Controller/PasswordResetTest.php
new file mode 100644
index 00000000..69f2f5e9
--- /dev/null
+++ b/src/tests/Feature/Controller/PasswordResetTest.php
@@ -0,0 +1,328 @@
+ 'passwordresettest@' . \config('app.domain')]);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return void
+ */
+ public function tearDown(): void
+ {
+ User::where('email', 'passwordresettest@' . \config('app.domain'))
+ ->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test password-reset/init with invalid input
+ *
+ * @return void
+ */
+ public function testPasswordResetInitInvalidInput()
+ {
+ // Empty input data
+ $data = [];
+
+ $response = $this->post('/api/auth/password-reset/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 invalid email
+ $data = [
+ 'email' => '@example.org',
+ ];
+
+ $response = $this->post('/api/auth/password-reset/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 valid but non-existing email
+ $data = [
+ 'email' => 'non-existing-password-reset@example.org',
+ ];
+
+ $response = $this->post('/api/auth/password-reset/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 valid email af an existing user with no external email
+ $data = [
+ 'email' => 'passwordresettest@' . \config('app.domain'),
+ ];
+
+ $response = $this->post('/api/auth/password-reset/init', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertArrayHasKey('email', $json['errors']);
+ }
+
+ /**
+ * Test password-reset/init with valid input
+ *
+ * @return array
+ */
+ public function testPasswordResetInitValidInput()
+ {
+ Queue::fake();
+
+ // Assert that no jobs were pushed...
+ Queue::assertNothingPushed();
+
+ // Add required external email address to user settings
+ $user = User::where('email', 'passwordresettest@' . \config('app.domain'))->first();
+ $user->setSetting('external_email', 'ext@email.com');
+
+ $data = [
+ 'email' => 'passwordresettest@' . \config('app.domain'),
+ ];
+
+ $response = $this->post('/api/auth/password-reset/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\PasswordResetEmail::class, 1);
+
+ // Assert the job has proper data assigned
+ Queue::assertPushed(\App\Jobs\PasswordResetEmail::class, function ($job) use ($user, &$code, $json) {
+ // Access protected property
+ $reflection = new \ReflectionClass($job);
+ $code = $reflection->getProperty('code');
+ $code->setAccessible(true);
+ $code = $code->getValue($job);
+
+ return $code->user->id === $user->id && $code->code == $json['code'];
+ });
+
+ return [
+ 'code' => $code
+ ];
+ }
+
+ /**
+ * Test password-reset/verify with invalid input
+ *
+ * @return void
+ */
+ public function testPasswordResetVerifyInvalidInput()
+ {
+ // Empty data
+ $data = [];
+
+ $response = $this->post('/api/auth/password-reset/verify', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json['errors']);
+ $this->assertArrayHasKey('short_code', $json['errors']);
+
+ // Add verification code and required external email address to user settings
+ $user = User::where('email', 'passwordresettest@' . \config('app.domain'))->first();
+ $code = new VerificationCode(['mode' => 'password-reset']);
+ $user->verificationcodes()->save($code);
+
+ // Data with existing code but missing short_code
+ $data = [
+ 'code' => $code->code,
+ ];
+
+ $response = $this->post('/api/auth/password-reset/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 code
+ $data = [
+ 'short_code' => '123456789',
+ 'code' => $code->code,
+ ];
+
+ $response = $this->post('/api/auth/password-reset/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 password-reset/verify with valid input
+ *
+ * @return void
+ */
+ public function testPasswordResetVerifyValidInput()
+ {
+ // Add verification code and required external email address to user settings
+ $user = User::where('email', 'passwordresettest@' . \config('app.domain'))->first();
+ $code = new VerificationCode(['mode' => 'password-reset']);
+ $user->verificationcodes()->save($code);
+
+ // Data with invalid code
+ $data = [
+ 'short_code' => $code->short_code,
+ 'code' => $code->code,
+ ];
+
+ $response = $this->post('/api/auth/password-reset/verify', $data);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+ $this->assertCount(1, $json);
+ $this->assertSame('success', $json['status']);
+ }
+
+ /**
+ * Test password-reset with invalid input
+ *
+ * @return void
+ */
+ public function testPasswordResetInvalidInput()
+ {
+ // Empty data
+ $data = [];
+
+ $response = $this->post('/api/auth/password-reset', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertArrayHasKey('password', $json['errors']);
+
+ $user = User::where('email', 'passwordresettest@' . \config('app.domain'))->first();
+ $code = new VerificationCode(['mode' => 'password-reset']);
+ $user->verificationcodes()->save($code);
+
+ // Data with existing code but missing password
+ $data = [
+ 'code' => $code->code,
+ ];
+
+ $response = $this->post('/api/auth/password-reset', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertArrayHasKey('password', $json['errors']);
+
+ // Data with existing code but wrong password confirmation
+ $data = [
+ 'code' => $code->code,
+ 'short_code' => $code->short_code,
+ 'password' => 'password',
+ 'password_confirmation' => 'passwrong',
+ ];
+
+ $response = $this->post('/api/auth/password-reset', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertArrayHasKey('password', $json['errors']);
+
+ // Data with invalid short code
+ $data = [
+ 'code' => $code->code,
+ 'short_code' => '123456789',
+ 'password' => 'password',
+ 'password_confirmation' => 'password',
+ ];
+
+ $response = $this->post('/api/auth/password-reset', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertArrayHasKey('short_code', $json['errors']);
+ }
+
+ /**
+ * Test password reset with valid input
+ *
+ * @return void
+ */
+ public function testPasswordResetValidInput()
+ {
+ $user = User::where('email', 'passwordresettest@' . \config('app.domain'))->first();
+ $code = new VerificationCode(['mode' => 'password-reset']);
+ $user->verificationcodes()->save($code);
+
+ $data = [
+ 'password' => 'test',
+ 'password_confirmation' => 'test',
+ 'code' => $code->code,
+ 'short_code' => $code->short_code,
+ ];
+
+ $response = $this->post('/api/auth/password-reset', $data);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+ $this->assertCount(4, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('bearer', $json['token_type']);
+ $this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0);
+ $this->assertNotEmpty($json['access_token']);
+
+ // Check if the code has been removed
+ $this->assertNull(VerificationCode::find($code->code));
+
+ // TODO: Check password before and after (?)
+
+ // TODO: Check if the access token works
+ }
+}
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
index eddd25df..4249d9e2 100644
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -1,405 +1,404 @@
'SignupControllerTest1@' . \config('app.domain')]);
}
/**
* {@inheritDoc}
*
* @return void
*/
public function tearDown(): void
{
User::where('email', 'signuplogin@' . \config('app.domain'))
->orWhere('email', 'SignupControllerTest1@' . \config('app.domain'))
->delete();
parent::tearDown();
}
/**
* Test signup initialization with invalid input
*
* @return void
*/
public function testSignupInitInvalidInput()
{
// Empty input data
$data = [];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
$this->assertArrayHasKey('name', $json['errors']);
// Data with missing name
$data = [
'email' => 'UsersApiControllerTest1@UsersApiControllerTest.com',
'password' => 'simple123',
'password_confirmation' => 'simple123'
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('name', $json['errors']);
// Data with invalid email (but not phone number)
$data = [
'email' => '@example.org',
'name' => 'Signup User',
'password' => 'simple123',
'password_confirmation' => 'simple123'
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
// TODO: Test phone validation
}
/**
* Test signup initialization with valid input
*
* @return array
*/
public function testSignupInitValidInput()
{
Queue::fake();
// Assert that no jobs were pushed...
Queue::assertNothingPushed();
$data = [
'email' => 'testuser@external.com',
'name' => 'Signup User',
'password' => 'simple123',
'password_confirmation' => 'simple123'
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertNotEmpty($json['code']);
// Assert the email sending job was pushed once
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1);
// Assert the job has proper data assigned
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) {
// Access protected property
$reflection = new \ReflectionClass($job);
$code = $reflection->getProperty('code');
$code->setAccessible(true);
$code = $code->getValue($job);
return $code->code === $json['code']
&& $code->data['email'] === $data['email']
&& $code->data['name'] === $data['name'];
});
return [
'code' => $json['code'],
'email' => $data['email'],
'name' => $data['name'],
];
}
/**
* Test signup code verification with invalid input
*
* @depends testSignupInitValidInput
* @return void
*/
public function testSignupVerifyInvalidInput(array $result)
{
// Empty data
$data = [];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('code', $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with existing code but missing short_code
$data = [
'code' => $result['code'],
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with invalid short_code
$data = [
'code' => $result['code'],
'short_code' => 'XXXX',
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// TODO: Test expired code
}
/**
* Test signup code verification with valid input
*
* @depends testSignupInitValidInput
*
* @return array
*/
public function testSignupVerifyValidInput(array $result)
{
$code = SignupCode::find($result['code']);
$data = [
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
$this->assertSame($result['email'], $json['email']);
$this->assertSame($result['name'], $json['name']);
return $result;
}
/**
* Test last signup step with invalid input
*
* @depends testSignupVerifyValidInput
* @return void
*/
public function testSignupInvalidInput(array $result)
{
// Empty data
$data = [];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
// Passwords do not match
$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(1, $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
// Login too short
$data = [
'login' => '1',
'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']);
// Login invalid
$data = [
'login' => 'żżżżż',
'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']);
// Data with invalid short_code
$data = [
'login' => 'TestLogin',
'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']);
}
/**
* Test last signup step with valid input (user creation)
*
* @depends testSignupVerifyValidInput
* @return void
*/
public function testSignupValidInput(array $result)
{
$identity = \strtolower('SignupLogin@') . \config('app.domain');
$code = SignupCode::find($result['code']);
$data = [
'login' => 'SignupLogin',
'password' => 'test',
'password_confirmation' => 'test',
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(4, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('bearer', $json['token_type']);
$this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0);
$this->assertNotEmpty($json['access_token']);
// Check if the code has been removed
$this->assertNull(SignupCode::where($result['code'])->first());
// Check if the user has been created
$user = User::where('email', $identity)->first();
$this->assertNotEmpty($user);
$this->assertSame($identity, $user->email);
$this->assertSame($result['name'], $user->name);
// Check external email in user settings
- $this->assertSame($result['email'], $user->getSetting('external_email', 'not set'));
+ $this->assertSame($result['email'], $user->getSetting('external_email'));
// TODO: Check if the access token works
}
/**
* List of email address validation cases for testValidateEmail()
*
* @return array Arguments for testValidateEmail()
*/
public function dataValidateEmail()
{
// To access config from dataProvider method we have to refreshApplication() first
$this->refreshApplication();
$domain = \config('app.domain');
return [
// general cases (invalid)
['', false, 'validation.emailinvalid'],
['example.org', false, 'validation.emailinvalid'],
['@example.org', false, 'validation.emailinvalid'],
['test@localhost', false, 'validation.emailinvalid'],
// general cases (valid)
['test@domain.tld', false, null],
['&@example.org', false, null],
// kolab identity cases
['admin@' . $domain, true, 'validation.emailexists'],
['administrator@' . $domain, true, 'validation.emailexists'],
['sales@' . $domain, true, 'validation.emailexists'],
['root@' . $domain, true, 'validation.emailexists'],
['&@' . $domain, true, 'validation.emailinvalid'],
['testnonsystemdomain@invalid.tld', true, 'validation.emailinvalid'],
// existing account
['SignupControllerTest1@' . $domain, true, 'validation.emailexists'],
// valid for signup
['test.test@' . $domain, true, null],
['test_test@' . $domain, true, null],
['test-test@' . $domain, true, null],
];
}
/**
* Signup email validation.
*
* Note: Technicly these are mostly unit tests, but let's keep it here for now.
* FIXME: Shall we do a http request for each case?
*
* @dataProvider dataValidateEmail
*/
public function testValidateEmail($email, $signup, $expected_result)
{
$method = new \ReflectionMethod('App\Http\Controllers\API\SignupController', 'validateEmail');
$method->setAccessible(true);
$is_phone = false;
$result = $method->invoke(new SignupController(), $email, $signup);
$this->assertSame($expected_result, $result);
}
}
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
index 0cfd399f..95e46ac9 100644
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -1,80 +1,79 @@
'UsersControllerTest1@UsersControllerTest.com'
]
);
$user->delete();
}
/**
- {@inheritDoc}
-
- @return void
+ * {@inheritDoc}
+ *
+ * @return void
*/
public function tearDown(): void
{
$user = User::firstOrCreate(
[
'email' => 'UsersControllerTest1@UsersControllerTest.com'
]
);
$user->delete();
parent::tearDown();
}
public function testListUsers()
{
$user = User::firstOrCreate(
[
'email' => 'UsersControllerTest1@UsersControllerTest.com'
]
);
$response = $this->actingAs($user)->get("api/v4/users");
$response->assertJsonCount(1);
$response->assertStatus(200);
}
public function testUserEntitlements()
{
$userA = User::firstOrCreate(
[
'email' => 'UserEntitlement2A@UserEntitlement.com'
]
);
$response = $this->actingAs($userA, 'api')->get("/api/v4/users/{$userA->id}");
$response->assertStatus(200);
$response->assertJson(['id' => $userA->id]);
$user = factory(User::class)->create();
$response = $this->actingAs($user)->get("/api/v4/users/{$userA->id}");
$response->assertStatus(404);
}
}
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
index e1d87bb9..553fea35 100644
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -1,76 +1,75 @@
first();
if ($domain) {
$domain->delete();
}
}
}
public function testDomainStatus()
{
$statuses = [ "new", "active", "confirmed", "suspended", "deleted" ];
$domains = \App\Utils::powerSet($statuses);
foreach ($domains as $namespace_elements) {
$namespace = implode('-', $namespace_elements) . '.com';
$status = 1;
if (in_array("new", $namespace_elements)) {
$status += Domain::STATUS_NEW;
}
if (in_array("active", $namespace_elements)) {
$status += Domain::STATUS_ACTIVE;
}
if (in_array("confirmed", $namespace_elements)) {
$status += Domain::STATUS_CONFIRMED;
}
if (in_array("suspended", $namespace_elements)) {
$status += Domain::STATUS_SUSPENDED;
}
if (in_array("deleted", $namespace_elements)) {
$status += Domain::STATUS_DELETED;
}
$domain = Domain::firstOrCreate(
[
'namespace' => $namespace,
'status' => $status,
'type' => Domain::TYPE_EXTERNAL
]
);
$this->assertTrue($domain->status > 1);
}
}
}
diff --git a/src/tests/Feature/EntitlementTest.php b/src/tests/Feature/EntitlementTest.php
index 5865c71a..0eb56532 100644
--- a/src/tests/Feature/EntitlementTest.php
+++ b/src/tests/Feature/EntitlementTest.php
@@ -1,114 +1,113 @@
'entitlement-test@kolabnow.com']
);
$user = User::firstOrCreate(
['email' => 'entitled-user@custom-domain.com']
);
$entitlement = Entitlement::firstOrCreate(
[
'owner_id' => $owner->id,
'user_id' => $user->id
]
);
$entitlement->delete();
$user->delete();
$owner->delete();
}
public function testUserAddEntitlement()
{
$sku_domain = Sku::firstOrCreate(
['title' => 'domain']
);
$sku_mailbox = Sku::firstOrCreate(
['title' => 'mailbox']
);
$owner = User::firstOrCreate(
['email' => 'entitlement-test@kolabnow.com']
);
$user = User::firstOrCreate(
['email' => 'entitled-user@custom-domain.com']
);
$this->assertTrue($owner->id != $user->id);
$wallets = $owner->wallets()->get();
$domain = Domain::firstOrCreate(
[
'namespace' => 'custom-domain.com',
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]
);
$entitlement_own_mailbox = Entitlement::firstOrCreate(
[
'owner_id' => $owner->id,
'entitleable_id' => $owner->id,
'entitleable_type' => User::class,
'wallet_id' => $wallets[0]->id,
'sku_id' => $sku_mailbox->id,
'description' => "Owner Mailbox Entitlement Test"
]
);
$entitlement_domain = Entitlement::firstOrCreate(
[
'owner_id' => $owner->id,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class,
'wallet_id' => $wallets[0]->id,
'sku_id' => $sku_domain->id,
'description' => "User Domain Entitlement Test"
]
);
$entitlement_mailbox = Entitlement::firstOrCreate(
[
'owner_id' => $owner->id,
'entitleable_id' => $user->id,
'entitleable_type' => User::class,
'wallet_id' => $wallets[0]->id,
'sku_id' => $sku_mailbox->id,
'description' => "User Mailbox Entitlement Test"
]
);
$owner->addEntitlement($entitlement_own_mailbox);
$owner->addEntitlement($entitlement_domain);
$owner->addEntitlement($entitlement_mailbox);
$this->assertTrue($owner->entitlements()->count() == 3);
$this->assertTrue($sku_domain->entitlements()->count() == 2);
$this->assertTrue($sku_mailbox->entitlements()->count() == 3);
$this->assertTrue($wallets[0]->entitlements()->count() == 3);
$this->assertTrue($wallets[0]->fresh()->balance < 0.00);
}
}
diff --git a/src/tests/Feature/Jobs/PasswordResetEmailTest.php b/src/tests/Feature/Jobs/PasswordResetEmailTest.php
new file mode 100644
index 00000000..5012864d
--- /dev/null
+++ b/src/tests/Feature/Jobs/PasswordResetEmailTest.php
@@ -0,0 +1,71 @@
+ 'PasswordReset@UserAccount.com'
+ ]);
+ $this->code = new VerificationCode([
+ 'mode' => 'password-reset',
+ ]);
+
+ $user->verificationcodes()->save($this->code);
+ $user->setSettings(['external_email' => 'etx@email.com']);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return void
+ */
+ public function tearDown(): void
+ {
+ $this->code->user->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test job handle
+ *
+ * @return void
+ */
+ public function testPasswordResetEmailHandle()
+ {
+ Mail::fake();
+
+ // Assert that no jobs were pushed...
+ Mail::assertNothingSent();
+
+ $job = new PasswordResetEmail($this->code);
+ $job->handle();
+
+ // Assert the email sending job was pushed once
+ Mail::assertSent(PasswordReset::class, 1);
+
+ // Assert the mail was sent to the code's email
+ Mail::assertSent(PasswordReset::class, function ($mail) {
+ return $mail->hasTo($this->code->user->getSetting('external_email'));
+ });
+ }
+}
diff --git a/src/tests/Feature/Jobs/SignupVerificationEmailTest.php b/src/tests/Feature/Jobs/SignupVerificationEmailTest.php
index 44477eef..4dfbc3bf 100644
--- a/src/tests/Feature/Jobs/SignupVerificationEmailTest.php
+++ b/src/tests/Feature/Jobs/SignupVerificationEmailTest.php
@@ -1,68 +1,67 @@
code = SignupCode::create([
'data' => [
'email' => 'SignupVerificationEmailTest1@' . \config('app.domain'),
'name' => "Test Job"
]
]);
}
/**
* {@inheritDoc}
*
* @return void
*/
public function tearDown(): void
{
$this->code->delete();
parent::tearDown();
}
/**
* Test job handle
*
* @return void
*/
public function testSignupVerificationEmailHandle()
{
Mail::fake();
// Assert that no jobs were pushed...
Mail::assertNothingSent();
$job = new SignupVerificationEmail($this->code);
$job->handle();
// Assert the email sending job was pushed once
Mail::assertSent(SignupVerification::class, 1);
// Assert the mail was sent to the code's email
Mail::assertSent(SignupVerification::class, function ($mail) {
return $mail->hasTo($this->code->data['email']);
});
}
}
diff --git a/src/tests/Feature/SignupCodeTest.php b/src/tests/Feature/SignupCodeTest.php
index 86efdac2..fea7b66f 100644
--- a/src/tests/Feature/SignupCodeTest.php
+++ b/src/tests/Feature/SignupCodeTest.php
@@ -1,43 +1,42 @@
[
'email' => 'User@email.org',
'name' => 'User Name',
]
];
$now = new \DateTime('now');
$code = SignupCode::create($data);
$this->assertFalse($code->isExpired());
$this->assertTrue(strlen($code->code) === SignupCode::CODE_LENGTH);
- $this->assertTrue(strlen($code->short_code) === env('SIGNUP_CODE_LENGTH', SignupCode::SHORTCODE_LENGTH));
+ $this->assertTrue(strlen($code->short_code) === env('VERIFICATION_CODE_LENGTH', SignupCode::SHORTCODE_LENGTH));
$this->assertSame($data['data'], $code->data);
$this->assertInstanceOf(\DateTime::class, $code->expires_at);
$this->assertSame(env('SIGNUP_CODE_EXPIRY', SignupCode::CODE_EXP_HOURS), $code->expires_at->diff($now)->h + 1);
$inst = SignupCode::find($code->code);
$this->assertInstanceOf(SignupCode::class, $inst);
$this->assertSame($inst->code, $code->code);
}
}
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
index 65e2ce84..791b05c7 100644
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -1,66 +1,70 @@
'UserAccountA@UserAccount.com'
]
);
$this->assertTrue($userA->wallets()->count() == 1);
$userA->wallets()->each(
function ($wallet) {
$userB = User::firstOrCreate(
[
'email' => 'UserAccountB@UserAccount.com'
]
);
$wallet->addController($userB);
}
);
$userB = User::firstOrCreate(
[
'email' => 'UserAccountB@UserAccount.com'
]
);
$this->assertTrue($userB->accounts()->get()[0]->id === $userA->wallets()->get()[0]->id);
}
public function testUserDomains()
{
$user = User::firstOrCreate(
[
'email' => 'john@kolab.org'
]
);
$domains = [];
foreach ($user->domains() as $domain) {
$domains[] = $domain->namespace;
}
$this->assertTrue(in_array('kolabnow.com', $domains));
$this->assertTrue(in_array('kolab.org', $domains));
}
+
+ public function testFindByEmail()
+ {
+ $this->markTestIncomplete('TODO');
+ }
}
diff --git a/src/tests/Feature/VerificationCodeTest.php b/src/tests/Feature/VerificationCodeTest.php
new file mode 100644
index 00000000..3a179618
--- /dev/null
+++ b/src/tests/Feature/VerificationCodeTest.php
@@ -0,0 +1,46 @@
+ 'UserAccountA@UserAccount.com']);
+ $data = [
+ 'user_id' => $user->id,
+ 'mode' => 'password-reset',
+ ];
+
+ $now = new \DateTime('now');
+
+ $code = VerificationCode::create($data);
+
+ $code_length = env('VERIFICATION_CODE_LENGTH', VerificationCode::SHORTCODE_LENGTH);
+ $code_exp_hrs = env('VERIFICATION_CODE_EXPIRY', VerificationCode::CODE_EXP_HOURS);
+
+ $this->assertFalse($code->isExpired());
+ $this->assertTrue(strlen($code->code) === VerificationCode::CODE_LENGTH);
+ $this->assertTrue(strlen($code->short_code) === $code_length);
+ $this->assertSame($data['mode'], $code->mode);
+ $this->assertSame($user->id, $code->user->id);
+ $this->assertInstanceOf(\DateTime::class, $code->expires_at);
+ $this->assertSame($code_exp_hrs, $code->expires_at->diff($now)->h + 1);
+
+ $inst = VerificationCode::find($code->code);
+
+ $this->assertInstanceOf(VerificationCode::class, $inst);
+ $this->assertSame($inst->code, $code->code);
+ }
+}
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
index 1c4be37a..7f71a64f 100644
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -1,243 +1,242 @@
users as $user) {
$_user = User::firstOrCreate(['email' => $user]);
$_user->delete();
}
}
public function tearDown(): void
{
foreach ($this->users as $user) {
$_user = User::firstOrCreate(['email' => $user]);
$_user->delete();
}
parent::tearDown();
}
/**
Verify a wallet is created, when a user is created.
@return void
*/
public function testCreateUserCreatesWallet()
{
$user = User::firstOrCreate(
[
'email' => 'UserWallet1@UserWallet.com'
]
);
$this->assertTrue($user->wallets()->count() == 1);
}
/**
Verify a user can haz more wallets.
@return void
*/
public function testAddWallet()
{
$user = User::firstOrCreate(
[
'email' => 'UserWallet2@UserWallet.com'
]
);
$user->wallets()->save(
new Wallet(['currency' => 'USD'])
);
$this->assertTrue($user->wallets()->count() >= 2);
$user->wallets()->each(
function ($wallet) {
$this->assertTrue($wallet->balance === 0.00);
}
);
}
/**
Verify we can not delete a user wallet that holds balance.
@return void
*/
public function testDeleteWalletWithCredit()
{
$user = User::firstOrCreate(
[
'email' => 'UserWallet3@UserWallet.com'
]
);
$user->wallets()->each(
function ($wallet) {
$wallet->credit(1.00)->save();
}
);
$user->wallets()->each(
function ($wallet) {
$this->assertFalse($wallet->delete());
}
);
}
/**
Verify we can not delete a wallet that is the last wallet.
@return void
*/
public function testDeleteLastWallet()
{
$user = User::firstOrCreate(
[
'email' => 'UserWallet4@UserWallet.com'
]
);
$this->assertTrue($user->wallets()->count() == 1);
$user->wallets()->each(
function ($wallet) {
$this->assertFalse($wallet->delete());
}
);
}
/**
Verify we can remove a wallet that is an additional wallet.
@return void
*/
public function testDeleteAddtWallet()
{
$user = User::firstOrCreate(
[
'email' => 'UserWallet5@UserWallet.com'
]
);
$user->wallets()->save(
new Wallet(['currency' => 'USD'])
);
$user->wallets()->each(
function ($wallet) {
if ($wallet->currency == 'USD') {
$this->assertNotFalse($wallet->delete());
}
}
);
}
/**
Verify a wallet can be assigned a controller.
@return void
*/
public function testAddWalletController()
{
$userA = User::firstOrCreate(
[
'email' => 'WalletControllerA@WalletController.com'
]
);
$userA->wallets()->each(
function ($wallet) {
$userB = User::firstOrCreate(
[
'email' => 'WalletControllerB@WalletController.com'
]
);
$wallet->addController($userB);
}
);
$userB = User::firstOrCreate(
[
'email' => 'WalletControllerB@WalletController.com'
]
);
$this->assertTrue($userB->accounts()->count() == 1);
$aWallet = $userA->wallets()->get();
$bAccount = $userB->accounts()->get();
$this->assertTrue($bAccount[0]->id === $aWallet[0]->id);
}
/**
Verify controllers can also be removed from wallets.
@return void
*/
public function testRemoveWalletController()
{
$userA = User::firstOrCreate(
[
'email' => 'WalletController2A@WalletController.com'
]
);
$userA->wallets()->each(
function ($wallet) {
$userB = User::firstOrCreate(
[
'email' => 'WalletController2B@WalletController.com'
]
);
$wallet->addController($userB);
}
);
$userB = User::firstOrCreate(
[
'email' => 'WalletController2B@WalletController.com'
]
);
$userB->accounts()->each(
function ($wallet) {
$userB = User::firstOrCreate(
[
'email' => 'WalletController2B@WalletController.com'
]
);
$wallet->removeController($userB);
}
);
$this->assertTrue($userB->accounts()->count() == 0);
}
}
diff --git a/src/tests/Unit/DomainTest.php b/src/tests/Unit/DomainTest.php
index 5e8833f1..433b4c5c 100644
--- a/src/tests/Unit/DomainTest.php
+++ b/src/tests/Unit/DomainTest.php
@@ -1,70 +1,69 @@
'test.com',
'status' => \array_sum($domain_statuses),
'type' => Domain::TYPE_EXTERNAL
]
);
$this->assertTrue($domain->isNew() === in_array(Domain::STATUS_NEW, $domain_statuses));
$this->assertTrue($domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domain_statuses));
$this->assertTrue($domain->isConfirmed() === in_array(Domain::STATUS_CONFIRMED, $domain_statuses));
$this->assertTrue($domain->isSuspended() === in_array(Domain::STATUS_SUSPENDED, $domain_statuses));
$this->assertTrue($domain->isDeleted() === in_array(Domain::STATUS_DELETED, $domain_statuses));
}
}
/**
* Test basic Domain funtionality
*/
public function testDomainType()
{
$types = [
Domain::TYPE_PUBLIC,
Domain::TYPE_HOSTED,
Domain::TYPE_EXTERNAL,
];
$domains = \App\Utils::powerSet($types);
foreach ($domains as $domain_types) {
$domain = new Domain(
[
'namespace' => 'test.com',
'status' => Domain::STATUS_NEW,
'type' => \array_sum($domain_types),
]
);
$this->assertTrue($domain->isPublic() === in_array(Domain::TYPE_PUBLIC, $domain_types));
$this->assertTrue($domain->isHosted() === in_array(Domain::TYPE_HOSTED, $domain_types));
$this->assertTrue($domain->isExternal() === in_array(Domain::TYPE_EXTERNAL, $domain_types));
}
}
}
diff --git a/src/tests/Unit/Mail/PasswordResetTest.php b/src/tests/Unit/Mail/PasswordResetTest.php
new file mode 100644
index 00000000..cb5ff671
--- /dev/null
+++ b/src/tests/Unit/Mail/PasswordResetTest.php
@@ -0,0 +1,40 @@
+ 123456789,
+ 'mode' => 'password-reset',
+ 'code' => 'code',
+ 'short_code' => 'short-code',
+ ]);
+ $code->user = new User([
+ 'name' => 'User Name',
+ ]);
+
+ $mail = new PasswordReset($code);
+ $html = $mail->build()->render();
+
+ $url = \config('app.url') . '/login/reset/' . $code->short_code . '-' . $code->code;
+ $link = "$url";
+
+ $this->assertSame(\config('app.name') . ' Password Reset', $mail->subject);
+ $this->assertStringStartsWith('', $html);
+ $this->assertTrue(strpos($html, $link) > 0);
+ $this->assertTrue(strpos($html, $code->user->name) > 0);
+ }
+}
diff --git a/src/tests/Unit/Mail/SignupVerificationTest.php b/src/tests/Unit/Mail/SignupVerificationTest.php
index 603d1265..32138a21 100644
--- a/src/tests/Unit/Mail/SignupVerificationTest.php
+++ b/src/tests/Unit/Mail/SignupVerificationTest.php
@@ -1,39 +1,38 @@
'code',
'short_code' => 'short-code',
'data' => [
'email' => 'test@email',
'name' => 'Test Name',
],
]);
$mail = new SignupVerification($code);
$html = $mail->build()->render();
$url = \config('app.url') . '/signup/' . $code->short_code . '-' . $code->code;
- $link = "$url";
+ $link = "$url";
$this->assertSame(\config('app.name') . ' Registration', $mail->subject);
$this->assertStringStartsWith('', $html);
$this->assertTrue(strpos($html, $link) > 0);
$this->assertTrue(strpos($html, $code->data['name']) > 0);
}
}
diff --git a/src/tests/Unit/SignupCodeTest.php b/src/tests/Unit/SignupCodeTest.php
index b326a813..ed2ed2fb 100644
--- a/src/tests/Unit/SignupCodeTest.php
+++ b/src/tests/Unit/SignupCodeTest.php
@@ -1,24 +1,23 @@
assertTrue(is_string($code));
$this->assertTrue(strlen($code) === env('SIGNUP_CODE_LENGTH', SignupCode::SHORTCODE_LENGTH));
$this->assertTrue(strspn($code, env('SIGNUP_CODE_CHARS', SignupCode::SHORTCODE_CHARS)) === strlen($code));
}
}
diff --git a/src/tests/Unit/UtilsTest.php b/src/tests/Unit/UtilsTest.php
index 495471f1..7f10ef3b 100644
--- a/src/tests/Unit/UtilsTest.php
+++ b/src/tests/Unit/UtilsTest.php
@@ -1,84 +1,83 @@
assertIsArray($result);
$this->assertCount(0, $result);
$set = ["a1"];
$result = \App\Utils::powerSet($set);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$this->assertTrue(in_array(["a1"], $result));
$set = ["a1", "a2"];
$result = \App\Utils::powerSet($set);
$this->assertIsArray($result);
$this->assertCount(3, $result);
$this->assertTrue(in_array(["a1"], $result));
$this->assertTrue(in_array(["a2"], $result));
$this->assertTrue(in_array(["a1", "a2"], $result));
$set = ["a1", "a2", "a3"];
$result = \App\Utils::powerSet($set);
$this->assertIsArray($result);
$this->assertCount(7, $result);
$this->assertTrue(in_array(["a1"], $result));
$this->assertTrue(in_array(["a2"], $result));
$this->assertTrue(in_array(["a3"], $result));
$this->assertTrue(in_array(["a1", "a2"], $result));
$this->assertTrue(in_array(["a1", "a3"], $result));
$this->assertTrue(in_array(["a2", "a3"], $result));
$this->assertTrue(in_array(["a1", "a2", "a3"], $result));
}
/**
* Test for Utils::uuidInt()
*
* @return void
*/
public function testUuidInt()
{
$result = Utils::uuidInt();
$this->assertTrue(is_int($result));
$this->assertTrue($result > 0);
}
/**
* Test for Utils::uuidStr()
*
* @return void
*/
public function testUuidStr()
{
$result = Utils::uuidStr();
$this->assertTrue(is_string($result));
$this->assertTrue(strlen($result) === 36);
$this->assertTrue(preg_match('/[^a-f0-9-]/i', $result) === 0);
}
}
diff --git a/src/tests/Unit/VerificationCodeTest.php b/src/tests/Unit/VerificationCodeTest.php
new file mode 100644
index 00000000..a1902dd6
--- /dev/null
+++ b/src/tests/Unit/VerificationCodeTest.php
@@ -0,0 +1,26 @@
+assertTrue(is_string($code));
+ $this->assertTrue(strlen($code) === $code_length);
+ $this->assertTrue(strspn($code, $code_chars) === strlen($code));
+ }
+}