diff --git a/src/app/Http/Controllers/API/UsersController.php b/src/app/Http/Controllers/API/UsersController.php index f845897c..09cdbdbd 100644 --- a/src/app/Http/Controllers/API/UsersController.php +++ b/src/app/Http/Controllers/API/UsersController.php @@ -1,247 +1,247 @@ 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() { $user = $this->guard()->user(); $response = $user->toArray(); $response['statusInfo'] = self::statusInfo($user); return response()->json($response); } /** * 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); } /** * User status (extended) information * * @param \App\User $user User object * * @return array Status information */ public static function statusInfo(User $user): array { $status = 'new'; $process = []; $steps = [ 'user-new' => true, 'user-ldap-ready' => 'isLdapReady', 'user-imap-ready' => 'isImapReady', ]; if ($user->isDeleted()) { $status = 'deleted'; } elseif ($user->isSuspended()) { $status = 'suspended'; } elseif ($user->isActive()) { $status = 'active'; } list ($local, $domain) = explode('@', $user->email); $domain = Domain::where('namespace', $domain)->first(); // If that is not a public domain, add domain specific steps if (!$domain->isPublic()) { $steps['domain-new'] = true; $steps['domain-ldap-ready'] = 'isLdapReady'; // $steps['domain-verified'] = 'isVerified'; $steps['domain-confirmed'] = 'isConfirmed'; } // Create a process check list foreach ($steps as $step_name => $func) { $object = strpos($step_name, 'user-') === 0 ? $user : $domain; $step = [ 'label' => $step_name, 'title' => __("app.process-{$step_name}"), - 'state' => false,//is_bool($func) ? $func : $object->{$func}(), + 'state' => is_bool($func) ? $func : $object->{$func}(), ]; if ($step_name == 'domain-confirmed' && !$step['state']) { $step['link'] = "/domain/{$domain->id}"; } $process[] = $step; } return [ 'process' => $process, 'status' => $status, ]; } /** * Get the guard to be used during authentication. * * @return \Illuminate\Contracts\Auth\Guard */ public function guard() { return Auth::guard(); } } diff --git a/src/app/Jobs/ProcessDomainCreate.php b/src/app/Jobs/DomainCreate.php similarity index 95% rename from src/app/Jobs/ProcessDomainCreate.php rename to src/app/Jobs/DomainCreate.php index ae61cec0..6962a2d4 100644 --- a/src/app/Jobs/ProcessDomainCreate.php +++ b/src/app/Jobs/DomainCreate.php @@ -1,53 +1,53 @@ domain = $domain; } /** * Execute the job. * * @return void */ public function handle() { if (!$this->domain->isLdapReady()) { LDAP::createDomain($this->domain); $this->domain->status |= Domain::STATUS_LDAP_READY; $this->domain->save(); } } } diff --git a/src/app/Jobs/ProcessDomainVerify.php b/src/app/Jobs/DomainVerify.php similarity index 95% rename from src/app/Jobs/ProcessDomainVerify.php rename to src/app/Jobs/DomainVerify.php index 2cc23885..87a20635 100644 --- a/src/app/Jobs/ProcessDomainVerify.php +++ b/src/app/Jobs/DomainVerify.php @@ -1,51 +1,51 @@ domain = $domain; } /** * Execute the job. * * @return void */ public function handle() { $this->domain->verify(); // TODO: What should happen if the domain is not registered yet? // Should we start a new job with some specified delay? // Or we just give the user a button to start verification again? } } diff --git a/src/app/Jobs/ProcessUserCreate.php b/src/app/Jobs/UserCreate.php similarity index 95% rename from src/app/Jobs/ProcessUserCreate.php rename to src/app/Jobs/UserCreate.php index d53fd03c..da34a45d 100644 --- a/src/app/Jobs/ProcessUserCreate.php +++ b/src/app/Jobs/UserCreate.php @@ -1,54 +1,54 @@ user = $user; } /** * Execute the job. * * @return void */ public function handle() { if (!$this->user->isLdapReady()) { LDAP::createUser($this->user); $this->user->status |= User::STATUS_LDAP_READY; $this->user->save(); } } } diff --git a/src/app/Jobs/ProcessUserDelete.php b/src/app/Jobs/UserDelete.php similarity index 94% rename from src/app/Jobs/ProcessUserDelete.php rename to src/app/Jobs/UserDelete.php index 8720d43c..eb6dfbd9 100644 --- a/src/app/Jobs/ProcessUserDelete.php +++ b/src/app/Jobs/UserDelete.php @@ -1,46 +1,46 @@ user = $user; } /** * Execute the job. * * @return void */ public function handle() { // } } diff --git a/src/app/Jobs/ProcessUserRead.php b/src/app/Jobs/UserRead.php similarity index 94% rename from src/app/Jobs/ProcessUserRead.php rename to src/app/Jobs/UserRead.php index feef61c5..c8d84f16 100644 --- a/src/app/Jobs/ProcessUserRead.php +++ b/src/app/Jobs/UserRead.php @@ -1,46 +1,46 @@ user = $user; } /** * Execute the job. * * @return void */ public function handle() { // } } diff --git a/src/app/Jobs/ProcessUserUpdate.php b/src/app/Jobs/UserUpdate.php similarity index 95% rename from src/app/Jobs/ProcessUserUpdate.php rename to src/app/Jobs/UserUpdate.php index b0928450..a1d6a7cc 100644 --- a/src/app/Jobs/ProcessUserUpdate.php +++ b/src/app/Jobs/UserUpdate.php @@ -1,47 +1,47 @@ user = $user; } /** * Execute the job. * * @return void */ public function handle() { LDAP::updateUser($this->user); } } diff --git a/src/app/Jobs/ProcessUserVerify.php b/src/app/Jobs/UserVerify.php similarity index 96% rename from src/app/Jobs/ProcessUserVerify.php rename to src/app/Jobs/UserVerify.php index 8173f60f..55717580 100644 --- a/src/app/Jobs/ProcessUserVerify.php +++ b/src/app/Jobs/UserVerify.php @@ -1,55 +1,55 @@ user = $user; } /** * Execute the job. * * @return void */ public function handle() { if (!$this->user->isImapReady()) { if (IMAP::verifyAccount($this->user->email)) { $this->user->status |= User::STATUS_IMAP_READY; $this->user->status |= User::STATUS_ACTIVE; $this->user->save(); } } } } diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php index 4baf2f54..cec4b603 100644 --- a/src/app/Observers/DomainObserver.php +++ b/src/app/Observers/DomainObserver.php @@ -1,96 +1,96 @@ {$domain->getKeyName()} = $allegedly_unique; break; } } $domain->status |= Domain::STATUS_NEW; } /** * Handle the domain "created" event. * * @param \App\Domain $domain The domain. * * @return void */ public function created(Domain $domain) { // Create domain record in LDAP, then check if it exists in DNS /* $chain = [ - new \App\Jobs\ProcessDomainVerify($domain), + new \App\Jobs\DomainVerify($domain), ]; - \App\Jobs\ProcessDomainCreate::withChain($chain)->dispatch($domain); + \App\Jobs\DomainCreate::withChain($chain)->dispatch($domain); */ - \App\Jobs\ProcessDomainCreate::dispatch($domain); + \App\Jobs\DomainCreate::dispatch($domain); } /** * Handle the domain "updated" event. * * @param \App\Domain $domain The domain. * * @return void */ public function updated(Domain $domain) { // } /** * Handle the domain "deleted" event. * * @param \App\Domain $domain The domain. * * @return void */ public function deleted(Domain $domain) { // } /** * Handle the domain "restored" event. * * @param \App\Domain $domain The domain. * * @return void */ public function restored(Domain $domain) { // } /** * Handle the domain "force deleted" event. * * @param \App\Domain $domain The domain. * * @return void */ public function forceDeleted(Domain $domain) { // } } diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php index c2fc54af..a69f1195 100644 --- a/src/app/Observers/UserObserver.php +++ b/src/app/Observers/UserObserver.php @@ -1,105 +1,105 @@ {$user->getKeyName()} = $allegedly_unique; break; } } $user->status |= User::STATUS_NEW; // can't dispatch job here because it'll fail serialization } /** * Handle the "created" event. * * Ensures the user has at least one wallet. * * Should ensure some basic settings are available as well. * * @param \App\User $user The user created. * * @return void */ public function created(User $user) { // FIXME: Actual proper settings $user->setSettings( [ 'country' => 'CH', 'currency' => 'CHF', 'first_name' => '', 'last_name' => '', 'billing_address' => '', 'organization' => '' ] ); $user->wallets()->create(); // Create user record in LDAP, then check if the account is created in IMAP $chain = [ - new \App\Jobs\ProcessUserVerify($user), + new \App\Jobs\UserVerify($user), ]; - \App\Jobs\ProcessUserCreate::withChain($chain)->dispatch($user); + \App\Jobs\UserCreate::withChain($chain)->dispatch($user); } /** * Handle the "deleting" event. * * @param User $user The user that is being deleted. * * @return void */ public function deleting(User $user) { - // TODO \App\Jobs\ProcessUserDelete::dispatch($user); + // TODO \App\Jobs\UserDelete::dispatch($user); } /** * Handle the "retrieving" event. * * @param User $user The user that is being retrieved. * * @todo This is useful for audit. * * @return void */ public function retrieving(User $user) { - // TODO \App\Jobs\ProcessUserRead::dispatch($user); + // TODO \App\Jobs\UserRead::dispatch($user); } /** * Handle the "updating" event. * * @param User $user The user that is being updated. * * @return void */ public function updating(User $user) { - \App\Jobs\ProcessUserUpdate::dispatch($user); + \App\Jobs\UserUpdate::dispatch($user); } } diff --git a/src/app/Observers/UserSettingObserver.php b/src/app/Observers/UserSettingObserver.php index 3812b628..d228114f 100644 --- a/src/app/Observers/UserSettingObserver.php +++ b/src/app/Observers/UserSettingObserver.php @@ -1,63 +1,63 @@ user); + \App\Jobs\UserUpdate::dispatch($userSetting->user); } /** * Handle the user setting "deleted" event. * * @param \App\UserSetting $userSetting * @return void */ public function deleted(UserSetting $userSetting) { // } /** * Handle the user setting "restored" event. * * @param \App\UserSetting $userSetting * @return void */ public function restored(UserSetting $userSetting) { // } /** * Handle the user setting "force deleted" event. * * @param \App\UserSetting $userSetting * @return void */ public function forceDeleted(UserSetting $userSetting) { // } } diff --git a/src/phpunit.xml b/src/phpunit.xml index 2eccde90..6f0dc481 100644 --- a/src/phpunit.xml +++ b/src/phpunit.xml @@ -1,35 +1,35 @@ tests/Unit tests/Feature - tests/Feature/Jobs/ProcessDomainVerifyTest.php + tests/Feature/Jobs/DomainVerifyTest.php ./app diff --git a/src/tests/Feature/Controller/PasswordResetTest.php b/src/tests/Feature/Controller/PasswordResetTest.php index 7e3e371e..a2528352 100644 --- a/src/tests/Feature/Controller/PasswordResetTest.php +++ b/src/tests/Feature/Controller/PasswordResetTest.php @@ -1,328 +1,328 @@ getTestUser('passwordresettest@' . \config('app.domain')); } /** * {@inheritDoc} */ public function tearDown(): void { User::where('email', 'passwordresettest@' . \config('app.domain')) ->delete(); } /** * Test password-reset/init with invalid input */ public function testPasswordResetInitInvalidInput(): void { // 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) { $code = TestCase::getObjectProperty($job, 'code'); 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); Queue::fake(); Queue::assertNothingPushed(); $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']); - Queue::assertPushed(\App\Jobs\ProcessUserUpdate::class, 1); - Queue::assertPushed(\App\Jobs\ProcessUserUpdate::class, function ($job) use ($user) { + Queue::assertPushed(\App\Jobs\UserUpdate::class, 1); + Queue::assertPushed(\App\Jobs\UserUpdate::class, function ($job) use ($user) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->id === $user->id && $job_user->email === $user->email && $job_user->password_ldap != $user->password_ldap; }); // 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 cc78c2f2..bdbe91fc 100644 --- a/src/tests/Feature/Controller/SignupTest.php +++ b/src/tests/Feature/Controller/SignupTest.php @@ -1,635 +1,635 @@ getTestUser("SignupControllerTest1@$domain"); } /** * {@inheritDoc} */ public function tearDown(): void { $domain = self::getPublicDomain(); User::where('email', "signuplogin@$domain") ->orWhere('email', "SignupControllerTest1@$domain") ->orWhere('email', 'admin@external.com') ->delete(); Domain::where('namespace', 'signup-domain.com') ->orWhere('namespace', 'external.com') ->delete(); } /** * Return a public domain for signup tests */ public function getPublicDomain(): string { if (!self::$domain) { $this->refreshApplication(); $public_domains = Domain::getPublicDomains(); self::$domain = reset($public_domains); if (empty(self::$domain)) { self::$domain = 'signup-domain.com'; Domain::create([ 'namespace' => self::$domain, 'status' => Domain::STATUS_ACTIVE, 'type' => Domain::TYPE_PUBLIC, ]); } } return self::$domain; } /** * Test fetching plans for signup * * @return void */ public function testSignupPlans() { // Note: this uses plans that already have been seeded into the DB $response = $this->get('/api/auth/signup/plans'); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertCount(2, $json['plans']); $this->assertArrayHasKey('title', $json['plans'][0]); $this->assertArrayHasKey('name', $json['plans'][0]); $this->assertArrayHasKey('description', $json['plans'][0]); $this->assertArrayHasKey('button', $json['plans'][0]); } /** * Test signup initialization with invalid input * * @return void */ public function testSignupInitInvalidInput() { // Empty input data $data = []; $response = $this->post('/api/auth/signup/init', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertArrayHasKey('email', $json['errors']); $this->assertArrayHasKey('name', $json['errors']); // Data with missing name $data = [ 'email' => 'UsersApiControllerTest1@UsersApiControllerTest.com', ]; $response = $this->post('/api/auth/signup/init', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertArrayHasKey('name', $json['errors']); // Data with invalid email (but not phone number) $data = [ 'email' => '@example.org', 'name' => 'Signup User', ]; $response = $this->post('/api/auth/signup/init', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertArrayHasKey('email', $json['errors']); // TODO: Test phone validation } /** * Test signup initialization with valid input * * @return array */ public function testSignupInitValidInput() { Queue::fake(); // Assert that no jobs were pushed... Queue::assertNothingPushed(); $data = [ 'email' => 'testuser@external.com', 'name' => 'Signup User', 'plan' => 'individual', ]; $response = $this->post('/api/auth/signup/init', $data); $json = $response->json(); $response->assertStatus(200); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertNotEmpty($json['code']); // Assert the email sending job was pushed once Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); // Assert the job has proper data assigned Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { $code = TestCase::getObjectProperty($job, 'code'); return $code->code === $json['code'] && $code->data['plan'] === $data['plan'] && $code->data['email'] === $data['email'] && $code->data['name'] === $data['name']; }); return [ 'code' => $json['code'], 'email' => $data['email'], 'name' => $data['name'], 'plan' => $data['plan'], ]; } /** * Test signup code verification with invalid input * * @depends testSignupInitValidInput * @return void */ public function testSignupVerifyInvalidInput(array $result) { // Empty data $data = []; $response = $this->post('/api/auth/signup/verify', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertArrayHasKey('code', $json['errors']); $this->assertArrayHasKey('short_code', $json['errors']); // Data with existing code but missing short_code $data = [ 'code' => $result['code'], ]; $response = $this->post('/api/auth/signup/verify', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertArrayHasKey('short_code', $json['errors']); // Data with invalid short_code $data = [ 'code' => $result['code'], 'short_code' => 'XXXX', ]; $response = $this->post('/api/auth/signup/verify', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertArrayHasKey('short_code', $json['errors']); // TODO: Test expired code } /** * Test signup code verification with valid input * * @depends testSignupInitValidInput * * @return array */ public function testSignupVerifyValidInput(array $result) { $code = SignupCode::find($result['code']); $data = [ 'code' => $code->code, 'short_code' => $code->short_code, ]; $response = $this->post('/api/auth/signup/verify', $data); $json = $response->json(); $response->assertStatus(200); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame($result['email'], $json['email']); $this->assertSame($result['name'], $json['name']); $this->assertSame(false, $json['is_domain']); $this->assertTrue(is_array($json['domains']) && !empty($json['domains'])); return $result; } /** * Test last signup step with invalid input * * @depends testSignupVerifyValidInput * @return void */ public function testSignupInvalidInput(array $result) { // Empty data $data = []; $response = $this->post('/api/auth/signup', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(3, $json['errors']); $this->assertArrayHasKey('login', $json['errors']); $this->assertArrayHasKey('password', $json['errors']); $this->assertArrayHasKey('domain', $json['errors']); $domain = $this->getPublicDomain(); // Passwords do not match and missing domain $data = [ 'login' => 'test', 'password' => 'test', 'password_confirmation' => 'test2', ]; $response = $this->post('/api/auth/signup', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertArrayHasKey('password', $json['errors']); $this->assertArrayHasKey('domain', $json['errors']); $domain = $this->getPublicDomain(); // Login too short $data = [ 'login' => '1', 'domain' => $domain, 'password' => 'test', 'password_confirmation' => 'test', ]; $response = $this->post('/api/auth/signup', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertArrayHasKey('login', $json['errors']); // Missing codes $data = [ 'login' => 'login-valid', 'domain' => $domain, 'password' => 'test', 'password_confirmation' => 'test', ]; $response = $this->post('/api/auth/signup', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertArrayHasKey('code', $json['errors']); $this->assertArrayHasKey('short_code', $json['errors']); // Data with invalid short_code $data = [ 'login' => 'TestLogin', 'domain' => $domain, 'password' => 'test', 'password_confirmation' => 'test', 'code' => $result['code'], 'short_code' => 'XXXX', ]; $response = $this->post('/api/auth/signup', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertArrayHasKey('short_code', $json['errors']); // Valid code, invalid login $code = SignupCode::find($result['code']); $data = [ 'login' => 'żżżżżż', 'domain' => $domain, 'password' => 'test', 'password_confirmation' => 'test', 'code' => $result['code'], 'short_code' => $code->short_code, ]; $response = $this->post('/api/auth/signup', $data); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertArrayHasKey('login', $json['errors']); } /** * Test last signup step with valid input (user creation) * * @depends testSignupVerifyValidInput * @return void */ public function testSignupValidInput(array $result) { Queue::fake(); $domain = $this->getPublicDomain(); $identity = \strtolower('SignupLogin@') . $domain; $code = SignupCode::find($result['code']); $data = [ 'login' => 'SignupLogin', 'domain' => $domain, 'password' => 'test', 'password_confirmation' => 'test', 'code' => $code->code, 'short_code' => $code->short_code, ]; $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']); - Queue::assertPushed(\App\Jobs\ProcessUserCreate::class, 1); - Queue::assertPushed(\App\Jobs\ProcessUserCreate::class, function ($job) use ($data) { + Queue::assertPushed(\App\Jobs\UserCreate::class, 1); + Queue::assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($data) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->email === \strtolower($data['login'] . '@' . $data['domain']); }); // Check if the code has been removed $this->assertNull(SignupCode::where($result['code'])->first()); // Check if the user has been created $user = User::where('email', $identity)->first(); $this->assertNotEmpty($user); $this->assertSame($identity, $user->email); $this->assertSame($result['name'], $user->name); // Check external email in user settings $this->assertSame($result['email'], $user->getSetting('external_email')); // TODO: Check SKUs/Plan // TODO: Check if the access token works } /** * Test signup for a group (custom domain) account * * @return void */ public function testSignupGroupAccount() { Queue::fake(); // Initial signup request $user_data = $data = [ 'email' => 'testuser@external.com', 'name' => 'Signup User', 'plan' => 'group', ]; $response = $this->post('/api/auth/signup/init', $data); $json = $response->json(); $response->assertStatus(200); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertNotEmpty($json['code']); // Assert the email sending job was pushed once Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); // Assert the job has proper data assigned Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { $code = TestCase::getObjectProperty($job, 'code'); return $code->code === $json['code'] && $code->data['plan'] === $data['plan'] && $code->data['email'] === $data['email'] && $code->data['name'] === $data['name']; }); // Verify the code $code = SignupCode::find($json['code']); $data = [ 'code' => $code->code, 'short_code' => $code->short_code, ]; $response = $this->post('/api/auth/signup/verify', $data); $result = $response->json(); $response->assertStatus(200); $this->assertCount(5, $result); $this->assertSame('success', $result['status']); $this->assertSame($user_data['email'], $result['email']); $this->assertSame($user_data['name'], $result['name']); $this->assertSame(true, $result['is_domain']); $this->assertSame([], $result['domains']); // Final signup request $login = 'admin'; $domain = 'external.com'; $data = [ 'login' => $login, 'domain' => $domain, 'password' => 'test', 'password_confirmation' => 'test', 'code' => $code->code, 'short_code' => $code->short_code, ]; $response = $this->post('/api/auth/signup', $data); $result = $response->json(); $response->assertStatus(200); $this->assertCount(4, $result); $this->assertSame('success', $result['status']); $this->assertSame('bearer', $result['token_type']); $this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0); $this->assertNotEmpty($result['access_token']); - Queue::assertPushed(\App\Jobs\ProcessDomainCreate::class, 1); - Queue::assertPushed(\App\Jobs\ProcessDomainCreate::class, function ($job) use ($domain) { + Queue::assertPushed(\App\Jobs\DomainCreate::class, 1); + Queue::assertPushed(\App\Jobs\DomainCreate::class, function ($job) use ($domain) { $job_domain = TestCase::getObjectProperty($job, 'domain'); return $job_domain->namespace === $domain; }); - Queue::assertPushed(\App\Jobs\ProcessUserCreate::class, 1); - Queue::assertPushed(\App\Jobs\ProcessUserCreate::class, function ($job) use ($data) { + Queue::assertPushed(\App\Jobs\UserCreate::class, 1); + Queue::assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($data) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->email === $data['login'] . '@' . $data['domain']; }); // Check if the code has been removed $this->assertNull(SignupCode::find($code->id)); // Check if the user has been created $user = User::where('email', $login . '@' . $domain)->first(); $this->assertNotEmpty($user); $this->assertSame($user_data['name'], $user->name); // Check domain record // Check external email in user settings $this->assertSame($user_data['email'], $user->getSetting('external_email')); // TODO: Check SKUs/Plan // TODO: Check if the access token works } /** * List of email address validation cases for testValidateEmail() * * @return array Arguments for testValidateEmail() */ public function dataValidateEmail() { return [ // invalid ['', 'validation.emailinvalid'], ['example.org', 'validation.emailinvalid'], ['@example.org', 'validation.emailinvalid'], ['test@localhost', 'validation.emailinvalid'], // valid ['test@domain.tld', null], ['&@example.org', null], ]; } /** * Signup email validation. * * Note: Technicly these are unit tests, but let's keep it here for now. * FIXME: Shall we do a http request for each case? * * @dataProvider dataValidateEmail */ public function testValidateEmail($email, $expected_result) { $method = new \ReflectionMethod('App\Http\Controllers\API\SignupController', 'validateEmail'); $method->setAccessible(true); $result = $method->invoke(new SignupController(), $email); $this->assertSame($expected_result, $result); } /** * List of login/domain validation cases for testValidateLogin() * * @return array Arguments for testValidateLogin() */ public function dataValidateLogin() { $domain = $this->getPublicDomain(); return [ // Individual account ['', $domain, false, ['login' => 'validation.logininvalid']], ['test123456', 'localhost', false, ['domain' => 'validation.domaininvalid']], ['test123456', 'unknown-domain.org', false, ['domain' => 'validation.domaininvalid']], ['test.test', $domain, false, null], ['test_test', $domain, false, null], ['test-test', $domain, false, null], ['admin', $domain, false, ['login' => 'validation.loginexists']], ['administrator', $domain, false, ['login' => 'validation.loginexists']], ['sales', $domain, false, ['login' => 'validation.loginexists']], ['root', $domain, false, ['login' => 'validation.loginexists']], // existing user ['SignupControllerTest1', $domain, false, ['login' => 'validation.loginexists']], // Domain account ['admin', 'kolabsys.com', true, null], ['testnonsystemdomain', 'invalid', true, ['domain' => 'validation.domaininvalid']], ['testnonsystemdomain', '.com', true, ['domain' => 'validation.domaininvalid']], // existing user ['SignupControllerTest1', $domain, true, ['domain' => 'validation.domainexists']], ]; } /** * Signup login/domain validation. * * Note: Technicly these include unit tests, but let's keep it here for now. * FIXME: Shall we do a http request for each case? * * @dataProvider dataValidateLogin */ public function testValidateLogin($login, $domain, $external, $expected_result) { $method = new \ReflectionMethod('App\Http\Controllers\API\SignupController', 'validateLogin'); $method->setAccessible(true); $result = $method->invoke(new SignupController(), $login, $domain, $external); $this->assertSame($expected_result, $result); } } diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php index db0de9fc..c39c3e6a 100644 --- a/src/tests/Feature/DomainTest.php +++ b/src/tests/Feature/DomainTest.php @@ -1,167 +1,167 @@ delete(); } /** * Test domain creating jobs */ public function testCreateJobs(): void { // Fake the queue, assert that no jobs were pushed... Queue::fake(); Queue::assertNothingPushed(); $domain = Domain::create([ 'namespace' => 'gmail.com', 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); - Queue::assertPushed(\App\Jobs\ProcessDomainCreate::class, 1); - Queue::assertPushed(\App\Jobs\ProcessDomainCreate::class, function ($job) use ($domain) { + Queue::assertPushed(\App\Jobs\DomainCreate::class, 1); + Queue::assertPushed(\App\Jobs\DomainCreate::class, function ($job) use ($domain) { $job_domain = TestCase::getObjectProperty($job, 'domain'); return $job_domain->id === $domain->id && $job_domain->namespace === $domain->namespace; }); /* - Queue::assertPushedWithChain(\App\Jobs\ProcessDomainCreate::class, [ - \App\Jobs\ProcessDomainVerify::class, + Queue::assertPushedWithChain(\App\Jobs\DomainCreate::class, [ + \App\Jobs\DomainVerify::class, ]); */ /* FIXME: Looks like we can't really do detailed assertions on chained jobs Another thing to consider is if we maybe should run these jobs independently (not chained) and make sure there's no race-condition in status update - Queue::assertPushed(\App\Jobs\ProcessDomainVerify::class, 1); - Queue::assertPushed(\App\Jobs\ProcessDomainVerify::class, function ($job) use ($domain) { + Queue::assertPushed(\App\Jobs\DomainVerify::class, 1); + Queue::assertPushed(\App\Jobs\DomainVerify::class, function ($job) use ($domain) { $job_domain = TestCase::getObjectProperty($job, 'domain'); return $job_domain->id === $domain->id && $job_domain->namespace === $domain->namespace; }); */ } /** * Tests getPublicDomains() method */ public function testGetPublicDomains(): void { $public_domains = Domain::getPublicDomains(); $this->assertNotContains('public-active.com', $public_domains); Queue::fake(); $domain = Domain::create([ 'namespace' => 'public-active.com', 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); // Public but non-active domain should not be returned $public_domains = Domain::getPublicDomains(); $this->assertNotContains('public-active.com', $public_domains); $domain = Domain::where('namespace', 'public-active.com')->first(); $domain->status = Domain::STATUS_ACTIVE; $domain->save(); // Public and active domain should be returned $public_domains = Domain::getPublicDomains(); $this->assertContains('public-active.com', $public_domains); } /** * Test domain (ownership) confirmation * * @group dns */ public function testConfirm(): void { /* DNS records for positive and negative tests - kolab.org: ci-success-cname A 212.103.80.148 ci-success-cname MX 10 mx01.kolabnow.com. ci-success-cname TXT "v=spf1 mx -all" kolab-verify.ci-success-cname CNAME 2b719cfa4e1033b1e1e132977ed4fe3e.ci-success-cname ci-failure-cname A 212.103.80.148 ci-failure-cname MX 10 mx01.kolabnow.com. kolab-verify.ci-failure-cname CNAME 2b719cfa4e1033b1e1e132977ed4fe3e.ci-failure-cname ci-success-txt A 212.103.80.148 ci-success-txt MX 10 mx01.kolabnow.com. ci-success-txt TXT "v=spf1 mx -all" ci-success-txt TXT "kolab-verify=de5d04ababb52d52e2519a2f16d11422" ci-failure-txt A 212.103.80.148 ci-failure-txt MX 10 mx01.kolabnow.com. kolab-verify.ci-failure-txt TXT "kolab-verify=de5d04ababb52d52e2519a2f16d11422" ci-failure-none A 212.103.80.148 ci-failure-none MX 10 mx01.kolabnow.com. */ Queue::fake(); $domain_props = ['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL]; $domain = $this->getTestDomain('ci-failure-none.kolab.org', $domain_props); $this->assertTrue($domain->confirm() === false); $this->assertTrue(!$domain->isConfirmed()); $domain = $this->getTestDomain('ci-failure-txt.kolab.org', $domain_props); $this->assertTrue($domain->confirm() === false); $this->assertTrue(!$domain->isConfirmed()); $domain = $this->getTestDomain('ci-failure-cname.kolab.org', $domain_props); $this->assertTrue($domain->confirm() === false); $this->assertTrue(!$domain->isConfirmed()); $domain = $this->getTestDomain('ci-success-txt.kolab.org', $domain_props); $this->assertTrue($domain->confirm()); $this->assertTrue($domain->isConfirmed()); $domain = $this->getTestDomain('ci-success-cname.kolab.org', $domain_props); $this->assertTrue($domain->confirm()); $this->assertTrue($domain->isConfirmed()); } } diff --git a/src/tests/Feature/Jobs/ProcessDomainCreateTest.php b/src/tests/Feature/Jobs/DomainCreateTest.php similarity index 92% rename from src/tests/Feature/Jobs/ProcessDomainCreateTest.php rename to src/tests/Feature/Jobs/DomainCreateTest.php index 55052bf3..b24a4a1a 100644 --- a/src/tests/Feature/Jobs/ProcessDomainCreateTest.php +++ b/src/tests/Feature/Jobs/DomainCreateTest.php @@ -1,48 +1,48 @@ delete(); } /** * Test job handle */ public function testHandle(): void { $domain = $this->getTestDomain( 'domain-create-test.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ] ); $this->assertFalse($domain->isLdapReady()); $mock = \Mockery::mock('alias:App\Backends\LDAP'); $mock->shouldReceive('createDomain') ->once() ->with($domain) ->andReturn(null); - $job = new ProcessDomainCreate($domain); + $job = new DomainCreate($domain); $job->handle(); $this->assertTrue($domain->fresh()->isLdapReady()); } } diff --git a/src/tests/Feature/Jobs/ProcessDomainVerifyTest.php b/src/tests/Feature/Jobs/DomainVerifyTest.php similarity index 91% rename from src/tests/Feature/Jobs/ProcessDomainVerifyTest.php rename to src/tests/Feature/Jobs/DomainVerifyTest.php index f8aa6f4f..07c882a2 100644 --- a/src/tests/Feature/Jobs/ProcessDomainVerifyTest.php +++ b/src/tests/Feature/Jobs/DomainVerifyTest.php @@ -1,69 +1,69 @@ orWhere('namespace', 'some-non-existing-domain.fff') ->delete(); } /** * Test job handle (existing domain) * * @group dns */ public function testHandle(): void { $domain = $this->getTestDomain( 'gmail.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ] ); $this->assertFalse($domain->isVerified()); - $job = new ProcessDomainVerify($domain); + $job = new DomainVerify($domain); $job->handle(); $this->assertTrue($domain->fresh()->isVerified()); } /** * Test job handle (non-existing domain) * * @group dns */ public function testHandleNonExisting(): void { $domain = $this->getTestDomain( 'some-non-existing-domain.fff', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ] ); $this->assertFalse($domain->isVerified()); - $job = new ProcessDomainVerify($domain); + $job = new DomainVerify($domain); $job->handle(); $this->assertFalse($domain->fresh()->isVerified()); } } diff --git a/src/tests/Feature/Jobs/UserCreateTest.php b/src/tests/Feature/Jobs/UserCreateTest.php index 79120ea1..0db6e16c 100644 --- a/src/tests/Feature/Jobs/UserCreateTest.php +++ b/src/tests/Feature/Jobs/UserCreateTest.php @@ -1,42 +1,42 @@ delete(); } /** * Test job handle */ public function testHandle(): void { $user = $this->getTestUser('new-job-user@' . \config('app.domain')); $this->assertFalse($user->isLdapReady()); $mock = \Mockery::mock('alias:App\Backends\LDAP'); $mock->shouldReceive('createUser') ->once() ->with($user) ->andReturn(null); - $job = new ProcessUserCreate($user); + $job = new UserCreate($user); $job->handle(); $this->assertTrue($user->fresh()->isLdapReady()); } } diff --git a/src/tests/Feature/Jobs/UserVerify.php b/src/tests/Feature/Jobs/UserVerifyTest.php similarity index 93% rename from src/tests/Feature/Jobs/UserVerify.php rename to src/tests/Feature/Jobs/UserVerifyTest.php index 16029339..c74f6d6d 100644 --- a/src/tests/Feature/Jobs/UserVerify.php +++ b/src/tests/Feature/Jobs/UserVerifyTest.php @@ -1,51 +1,51 @@ delete(); } /** * Test job handle */ public function testHandle(): void { $user = $this->getTestUser('new-job-user@' . \config('app.domain')); $this->assertFalse($user->isImapReady()); $mock = \Mockery::mock('alias:App\Backends\IMAP'); $mock->shouldReceive('verifyAccount') ->once() ->with($user->email) ->andReturn(false); - $job = new ProcessUserVerify($user); + $job = new UserVerify($user); $job->handle(); $this->assertTrue($user->fresh()->isImapReady() === false); $mock->shouldReceive('verifyAccount') ->once() ->with($user->email) ->andReturn(true); $job->handle(); $this->assertTrue($user->fresh()->isImapReady()); } } diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php index a499db77..f7770dfc 100644 --- a/src/tests/Feature/UserTest.php +++ b/src/tests/Feature/UserTest.php @@ -1,110 +1,110 @@ delete(); } /** * Verify user creation process */ public function testUserCreateJob(): void { // Fake the queue, assert that no jobs were pushed... Queue::fake(); Queue::assertNothingPushed(); $user = User::create([ 'email' => 'user-create-test@' . \config('app.domain') ]); - Queue::assertPushed(\App\Jobs\ProcessUserCreate::class, 1); - Queue::assertPushed(\App\Jobs\ProcessUserCreate::class, function ($job) use ($user) { + Queue::assertPushed(\App\Jobs\UserCreate::class, 1); + Queue::assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($user) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->id === $user->id && $job_user->email === $user->email; }); - Queue::assertPushedWithChain(\App\Jobs\ProcessUserCreate::class, [ - \App\Jobs\ProcessUserVerify::class, + Queue::assertPushedWithChain(\App\Jobs\UserCreate::class, [ + \App\Jobs\UserVerify::class, ]); /* FIXME: Looks like we can't really do detailed assertions on chained jobs Another thing to consider is if we maybe should run these jobs independently (not chained) and make sure there's no race-condition in status update - Queue::assertPushed(\App\Jobs\ProcessUserVerify::class, 1); - Queue::assertPushed(\App\Jobs\ProcessUserVerify::class, function ($job) use ($user) { + Queue::assertPushed(\App\Jobs\UserVerify::class, 1); + Queue::assertPushed(\App\Jobs\UserVerify::class, function ($job) use ($user) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->id === $user->id && $job_user->email === $user->email; }); */ } /** * Verify a wallet assigned a controller is among the accounts of the assignee. */ public function testListUserAccounts(): void { $userA = $this->getTestUser('UserAccountA@UserAccount.com'); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $this->assertTrue($userA->wallets()->count() == 1); $userA->wallets()->each( function ($wallet) use ($userB) { $wallet->addController($userB); } ); $this->assertTrue($userB->accounts()->get()[0]->id === $userA->wallets()->get()[0]->id); } public function testUserDomains(): void { $user = $this->getTestUser('john@kolab.org'); $domains = []; foreach ($user->domains() as $domain) { $domains[] = $domain->namespace; } $this->assertContains('kolabnow.com', $domains); $this->assertContains('kolab.org', $domains); } /** * Tests for User::findByEmail() */ public function testFindByEmail(): void { $user = $this->getTestUser('john@kolab.org'); $result = User::findByEmail('john'); $this->assertNull($result); $result = User::findByEmail('non-existing@email.com'); $this->assertNull($result); $result = User::findByEmail('john@kolab.org'); $this->assertInstanceOf(User::class, $result); $this->assertSame($user->id, $result->id); // TODO: Make sure searching is case-insensitive // TODO: Alias, eternal email } }