diff --git a/.env b/.env index 64fc7ef10f..e849e83801 100644 --- a/.env +++ b/.env @@ -69,8 +69,8 @@ _APP_STORAGE_ANTIVIRUS_PORT=3310 _APP_SMTP_HOST=maildev _APP_SMTP_PORT=1025 _APP_SMTP_SECURE= -_APP_SMTP_USERNAME= -_APP_SMTP_PASSWORD= +_APP_SMTP_USERNAME=user +_APP_SMTP_PASSWORD=password _APP_SMS_PROVIDER=sms://username:password@mock _APP_SMS_FROM=+123456789 _APP_SMS_PROJECTS_DENY_LIST= diff --git a/app/config/oAuthProviders.php b/app/config/oAuthProviders.php index d8dfc807b1..e6acd08c54 100644 --- a/app/config/oAuthProviders.php +++ b/app/config/oAuthProviders.php @@ -462,4 +462,15 @@ return [ 'mock' => true, 'class' => 'Appwrite\\Auth\\OAuth2\\Mock', ], + 'mock-unverified' => [ + 'name' => 'MockUnverified', + 'developers' => 'https://appwrite.io', + 'icon' => 'icon-appwrite', + 'enabled' => true, + 'sandbox' => false, + 'form' => false, + 'beta' => false, + 'mock' => true, + 'class' => 'Appwrite\\Auth\\OAuth2\\MockUnverified', + ], ]; diff --git a/app/config/roles.php b/app/config/roles.php index 3bf2297550..25a6bac4da 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -58,6 +58,8 @@ $admins = [ 'projects.write', 'keys.read', 'keys.write', + 'devKeys.read', + 'devKeys.write', 'webhooks.read', 'webhooks.write', 'locale.read', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 3e2512c073..b740cbe87d 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1639,9 +1639,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $failureRedirect(Exception::USER_UNAUTHORIZED, 'OAuth provider failed to return email.'); } - /** - * Is verified is not used yet, since we don't know after an account is created anymore if it was verified or not. - */ $isVerified = $oauth2->isEmailVerified($accessToken); $identity = $dbForProject->findOne('identities', [ @@ -1653,16 +1650,32 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $user = $dbForProject->getDocument('users', $identity->getAttribute('userId')); } - // If user is not found, check if there is an identity with the same provider user ID + // If user is not found, check if there is a user with the same email if ($user === false || $user->isEmpty()) { $userWithEmail = $dbForProject->findOne('users', [ Query::equal('email', [$email]), ]); if (!$userWithEmail->isEmpty()) { + if (!$isVerified) { + $failureRedirect(Exception::GENERAL_BAD_REQUEST); + } $user->setAttributes($userWithEmail->getArrayCopy()); } } + // If user is not found, check if there is an identity with the same email + if ($user === false || $user->isEmpty()) { + $identityWithMatchingEmail = $dbForProject->findOne('identities', [ + Query::equal('providerEmail', [$email]), + ]); + if (!$identityWithMatchingEmail->isEmpty()) { + if (!$isVerified) { + $failureRedirect(Exception::GENERAL_BAD_REQUEST); + } + $user->setAttributes($dbForProject->getDocument('users', $identityWithMatchingEmail->getAttribute('userId'))->getArrayCopy()); + } + } + if ($user === false || $user->isEmpty()) { // Last option -> create the user $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -1674,14 +1687,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } } - // Makes sure this email is not already used in another identity - $identityWithMatchingEmail = $dbForProject->findOne('identities', [ - Query::equal('providerEmail', [$email]), - ]); - if (!$identityWithMatchingEmail->isEmpty()) { - $failureRedirect(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ - } - try { $emailCanonical = new Email($email); } catch (Throwable) { @@ -1735,7 +1740,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'providerType' => MESSAGE_TYPE_EMAIL, 'identifier' => $email, ])); - } catch (Duplicate) { $failureRedirect(Exception::USER_ALREADY_EXISTS); } diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 37f7fdbc8b..c4d703d744 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -294,7 +294,7 @@ App::post('/v1/projects') // Hook allowing instant project mirroring during migration // Outside of migration, hook is not registered and has no effect - $hooks->trigger('afterProjectCreation', [ $project, $pools, $cache ]); + $hooks->trigger('afterProjectCreation', [$project, $pools, $cache]); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -2079,6 +2079,7 @@ App::patch('/v1/projects/:projectId/smtp') if ($enabled) { $mail = new PHPMailer(true); $mail->isSMTP(); + $mail->SMTPAuth = (!empty($username) && !empty($password)); $mail->Username = $username; $mail->Password = $password; $mail->Host = $host; @@ -2094,7 +2095,7 @@ App::patch('/v1/projects/:projectId/smtp') throw new Exception('Connection is not valid.'); } } catch (Throwable $error) { - throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, 'Could not connect to SMTP server: ' . $error->getMessage()); + throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, $error->getMessage()); } } @@ -2665,7 +2666,7 @@ App::patch('/v1/projects/:projectId/auth/session-invalidation') $auths = $project->getAttribute('auths', []); $auths['invalidateSessions'] = $enabled; $dbForPlatform->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); diff --git a/app/controllers/general.php b/app/controllers/general.php index 31647eb994..638df72419 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1034,7 +1034,8 @@ App::init() ->inject('dbForPlatform') ->inject('queueForCertificates') ->inject('platform') - ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform) { + ->inject('authorization') + ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization) { $hostname = $request->getHostname(); $cache = Config::getParam('hostnames', []); $platformHostnames = $platform['hostnames'] ?? []; @@ -1065,7 +1066,7 @@ App::init() } // 4. Check/create rule (requires DB access) - Authorization::disable(); + $authorization->disable(); try { // TODO: (@Meldiron) Remove after 1.7.x migration $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; @@ -1121,7 +1122,7 @@ App::init() } finally { $cache[$domain->get()] = true; Config::setParam('hostnames', $cache); - Authorization::reset(); + $authorization->reset(); } }); diff --git a/app/controllers/mock.php b/app/controllers/mock.php index d78bb61481..6f092a5d19 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -117,6 +117,28 @@ App::get('/v1/mock/tests/general/oauth2/user') 'id' => 1, 'name' => 'User Name', 'email' => 'useroauth@localhost.test', + 'verified' => true, + ]); + }); + +App::get('/v1/mock/tests/general/oauth2/user-unverified') + ->desc('OAuth2 User Unverified') + ->groups(['mock']) + ->label('scope', 'public') + ->label('docs', false) + ->param('token', '', new Text(100), 'OAuth2 Access Token.') + ->inject('response') + ->action(function (string $token, Response $response) { + + if ($token != '123456') { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid token'); + } + + $response->json([ + 'id' => 2, + 'name' => 'User Name Unverified', + 'email' => 'useroauthunverified@localhost.test', + 'verified' => false, ]); }); diff --git a/docker-compose.yml b/docker-compose.yml index 57007c4efc..3b935b84fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1131,6 +1131,9 @@ services: - "traefik.http.routers.appwrite_maildev_https.rule=Host(`mail.localhost`)" - "traefik.http.routers.appwrite_maildev_https.service=appwrite_maildev" - "traefik.http.routers.appwrite_maildev_https.tls=true" + environment: + - MAILDEV_INCOMING_USER=${_APP_SMTP_USERNAME} + - MAILDEV_INCOMING_PASS=${_APP_SMTP_PASSWORD} request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook) image: appwrite/requestcatcher:1.0.0 diff --git a/src/Appwrite/Auth/OAuth2/Mock.php b/src/Appwrite/Auth/OAuth2/Mock.php index 61ce41d1b7..8bbf3647a1 100644 --- a/src/Appwrite/Auth/OAuth2/Mock.php +++ b/src/Appwrite/Auth/OAuth2/Mock.php @@ -130,7 +130,9 @@ class Mock extends OAuth2 */ public function isEmailVerified(string $accessToken): bool { - return true; + $user = $this->getUser($accessToken); + + return $user['verified'] ?? true; } /** diff --git a/src/Appwrite/Auth/OAuth2/MockUnverified.php b/src/Appwrite/Auth/OAuth2/MockUnverified.php new file mode 100644 index 0000000000..93bea02416 --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/MockUnverified.php @@ -0,0 +1,30 @@ +user)) { + $user = $this->request('GET', 'http://localhost/' . $this->version . '/mock/tests/general/oauth2/user-unverified?token=' . \urlencode($accessToken)); + + $this->user = \json_decode($user, true); + } + + return $this->user; + } +} diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Create.php index 9332453eea..92bc329b16 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Create.php @@ -34,7 +34,7 @@ class Create extends Action ->setHttpPath('/v1/projects/:projectId/dev-keys') ->desc('Create dev key') ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'devKeys.write') ->label('sdk', new Method( namespace: 'projects', group: 'devKeys', diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Delete.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Delete.php index 2bfea6c55b..58ca0759e6 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Delete.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Delete.php @@ -28,7 +28,7 @@ class Delete extends Action ->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId') ->desc('Delete dev key') ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'devKeys.write') ->label('sdk', new Method( namespace: 'projects', group: 'devKeys', diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Get.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Get.php index 29cda90f66..6245b5b2dc 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Get.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Get.php @@ -28,7 +28,7 @@ class Get extends Action ->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId') ->desc('Get dev key') ->groups(['api', 'projects']) - ->label('scope', 'projects.read') + ->label('scope', 'devKeys.read') ->label('sdk', new Method( namespace: 'projects', group: 'devKeys', diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php index b13bc535dd..fbd35d0995 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php @@ -29,7 +29,7 @@ class Update extends Action ->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId') ->desc('Update dev key') ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'devKeys.write') ->label('sdk', new Method( namespace: 'projects', group: 'devKeys', diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/XList.php index 209387018b..0229fd845d 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/XList.php @@ -32,7 +32,7 @@ class XList extends Action ->setHttpPath('/v1/projects/:projectId/dev-keys') ->desc('List dev keys') ->groups(['api', 'projects']) - ->label('scope', 'projects.read') + ->label('scope', 'devKeys.read') ->label('sdk', new Method( namespace: 'projects', group: 'devKeys', diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index c2b4896814..52c53016d6 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -161,9 +161,9 @@ trait ProjectCustom 'senderEmail' => 'mailer@appwrite.io', 'senderName' => 'Mailer', 'host' => 'maildev', - 'port' => 1025, - 'username' => '', - 'password' => '', + 'port' => intval(System::getEnv('_APP_SMTP_PORT', "1025")), + 'username' => System::getEnv('_APP_SMTP_USERNAME', 'user'), + 'password' => System::getEnv('_APP_SMTP_PASSWORD', 'password'), ]); $project = [ diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 457799f991..d3aa8a4845 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -2183,6 +2183,157 @@ class AccountCustomClientTest extends Scope $this->assertEquals('tuvwxyz', $response['body']['providerRefreshToken']); $this->assertNotEquals($initialExpiry, $response['body']['providerAccessTokenExpiry']); + // Clean up - delete the user + $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals(204, $response['headers']['status-code']); + + return []; + } + + public function testOAuthUnverifiedEmailCannotLinkToExistingAccount() + { + $provider = 'mock-unverified'; + $appId = '1'; + $secret = '123456'; + + // First, create a user with the same email that the unverified OAuth will try to use + $email = 'useroauthunverified@localhost.test'; + $password = 'password'; + + $response = $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $existingUserId = $response['body']['$id']; + + // Enable the mock-unverified provider + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $this->getProject()['$id'] . '/oauth2', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]), [ + 'provider' => $provider, + 'appId' => $appId, + 'secret' => $secret, + 'enabled' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Attempt OAuth login with unverified email - should fail because existing user has same email + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success', + 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('failure', $response['body']['result']); + + // Clean up - delete the user + $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $existingUserId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals(204, $response['headers']['status-code']); + + return []; + } + + public function testOAuthVerifiedEmailCanLinkToExistingAccount() + { + $provider = 'mock'; + $appId = '1'; + $secret = '123456'; + $email = 'useroauth@localhost.test'; + + // Create a user with the same email that the verified OAuth will try to use + $response = $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => 'password', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $existingUserId = $response['body']['$id']; + + // Enable the mock provider + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $this->getProject()['$id'] . '/oauth2', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]), [ + 'provider' => $provider, + 'appId' => $appId, + 'secret' => $secret, + 'enabled' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Attempt OAuth login with verified email - should succeed and link to existing account + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success', + 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('success', $response['body']['result']); + + // Verify the OAuth identity was linked to the existing user + $sessionCookieKey = 'a_session_' . $this->getProject()['$id']; + $session = $response['cookies'][$sessionCookieKey]; + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($existingUserId, $response['body']['$id']); + $this->assertEquals($email, $response['body']['email']); + + // Clean up - delete the user + $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $existingUserId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals(204, $response['headers']['status-code']); + return []; } diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 9526c5a4da..f2122b4ba9 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -23,9 +23,9 @@ class ProjectsConsoleClientTest extends Scope use Async; /** - * @group devKeys * @group smtpAndTemplates - * @group projectsCRUD */ + * @group projectsCRUD + */ public function testCreateProject(): array { /** @@ -257,11 +257,11 @@ class ProjectsConsoleClientTest extends Scope 'search' => $id ])); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertEquals($response['body']['total'], 3); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(4, $response['body']['total']); $this->assertIsArray($response['body']['projects']); - $this->assertCount(3, $response['body']['projects']); - $this->assertEquals($response['body']['projects'][0]['name'], 'Project Test'); + $this->assertCount(4, $response['body']['projects']); + $this->assertEquals('Project Test', $response['body']['projects'][0]['name']); $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', @@ -271,9 +271,9 @@ class ProjectsConsoleClientTest extends Scope ])); $this->assertEquals($response['headers']['status-code'], 200); - $this->assertEquals(3, $response['body']['total']); + $this->assertEquals(4, $response['body']['total']); $this->assertIsArray($response['body']['projects']); - $this->assertCount(3, $response['body']['projects']); + $this->assertCount(4, $response['body']['projects']); $this->assertEquals($response['body']['projects'][0]['$id'], $data['projectId']); /** @@ -348,8 +348,8 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertCount(1, $response['body']['projects']); - $this->assertEquals('Project Test 2', $response['body']['projects'][0]['name']); + $this->assertCount(2, $response['body']['projects']); + $this->assertEquals('Team 1 Project', $response['body']['projects'][0]['name']); $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', @@ -376,7 +376,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertCount(4, $response['body']['projects']); + $this->assertCount(5, $response['body']['projects']); $this->assertEquals('Project Test 2', $response['body']['projects'][0]['name']); $this->assertEquals('Team 1 Project', $response['body']['projects'][1]['name']); @@ -387,9 +387,9 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertCount(4, $response['body']['projects']); + $this->assertCount(5, $response['body']['projects']); $this->assertEquals('Project Test', $response['body']['projects'][0]['name']); - $this->assertEquals('Team 1 Project', $response['body']['projects'][2]['name']); + $this->assertEquals('Original Project', $response['body']['projects'][2]['name']); $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', @@ -402,8 +402,8 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertCount(3, $response['body']['projects']); - $this->assertEquals('Team 1 Project', $response['body']['projects'][1]['name']); + $this->assertCount(4, $response['body']['projects']); + $this->assertEquals('Original Project', $response['body']['projects'][1]['name']); $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', @@ -618,6 +618,14 @@ class ProjectsConsoleClientTest extends Scope public function testUpdateProjectSMTP($data): array { $id = $data['projectId']; + $smtpHost = System::getEnv('_APP_SMTP_HOST', "maildev"); + $smtpPort = intval(System::getEnv('_APP_SMTP_PORT', "1025")); + $smtpUsername = System::getEnv('_APP_SMTP_USERNAME', 'user'); + $smtpPassword = System::getEnv('_APP_SMTP_PASSWORD', 'password'); + + /** + * Test for SUCCESS: Valid Credentials + */ $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -625,23 +633,23 @@ class ProjectsConsoleClientTest extends Scope 'enabled' => true, 'senderEmail' => 'mailer@appwrite.io', 'senderName' => 'Mailer', - 'host' => 'maildev', - 'port' => 1025, - 'username' => 'user', - 'password' => 'password', + 'host' => $smtpHost, + 'port' => $smtpPort, + 'username' => $smtpUsername, + 'password' => $smtpPassword, ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertTrue($response['body']['smtpEnabled']); $this->assertEquals('mailer@appwrite.io', $response['body']['smtpSenderEmail']); $this->assertEquals('Mailer', $response['body']['smtpSenderName']); - $this->assertEquals('maildev', $response['body']['smtpHost']); - $this->assertEquals(1025, $response['body']['smtpPort']); - $this->assertEquals('user', $response['body']['smtpUsername']); - $this->assertEquals('password', $response['body']['smtpPassword']); + $this->assertEquals($smtpHost, $response['body']['smtpHost']); + $this->assertEquals($smtpPort, $response['body']['smtpPort']); + $this->assertEquals($smtpUsername, $response['body']['smtpUsername']); + $this->assertEquals($smtpPassword, $response['body']['smtpPassword']); $this->assertEquals('', $response['body']['smtpSecure']); - /** Test Reading Project */ + // Check the project $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -651,12 +659,32 @@ class ProjectsConsoleClientTest extends Scope $this->assertTrue($response['body']['smtpEnabled']); $this->assertEquals('mailer@appwrite.io', $response['body']['smtpSenderEmail']); $this->assertEquals('Mailer', $response['body']['smtpSenderName']); - $this->assertEquals('maildev', $response['body']['smtpHost']); - $this->assertEquals(1025, $response['body']['smtpPort']); - $this->assertEquals('user', $response['body']['smtpUsername']); - $this->assertEquals('password', $response['body']['smtpPassword']); + $this->assertEquals($smtpHost, $response['body']['smtpHost']); + $this->assertEquals($smtpPort, $response['body']['smtpPort']); + $this->assertEquals($smtpUsername, $response['body']['smtpUsername']); + $this->assertEquals($smtpPassword, $response['body']['smtpPassword']); $this->assertEquals('', $response['body']['smtpSecure']); + /** + * Test for FAILURE: Invalid Credentials + */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'enabled' => true, + 'senderEmail' => 'fail@appwrite.io', + 'senderName' => 'Failing Mailer', + 'host' => $smtpHost, + 'port' => $smtpPort, + 'username' => 'invalid-user', + 'password' => 'bad-password', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals(Exception::PROJECT_SMTP_CONFIG_INVALID, $response['body']['type']); + $this->assertStringContainsStringIgnoringCase('Could not authenticate', $response['body']['message']); + return $data; } @@ -665,6 +693,11 @@ class ProjectsConsoleClientTest extends Scope */ public function testCreateProjectSMTPTests(): void { + $smtpHost = System::getEnv('_APP_SMTP_HOST', "maildev"); + $smtpPort = intval(System::getEnv('_APP_SMTP_PORT', "1025")); + $smtpUsername = System::getEnv('_APP_SMTP_USERNAME', 'user'); + $smtpPassword = System::getEnv('_APP_SMTP_PASSWORD', 'password'); + // Create a team $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ 'content-type' => 'application/json', @@ -699,10 +732,10 @@ class ProjectsConsoleClientTest extends Scope 'senderEmail' => 'custommailer@appwrite.io', 'senderName' => 'Custom Mailer', 'replyTo' => 'reply@appwrite.io', - 'host' => 'maildev', - 'port' => 1025, - 'username' => '', - 'password' => '', + 'host' => $smtpHost, + 'port' => $smtpPort, + 'username' => $smtpUsername, + 'password' => $smtpPassword, ]); $this->assertEquals(204, $response['headers']['status-code']); @@ -736,10 +769,10 @@ class ProjectsConsoleClientTest extends Scope 'senderEmail' => 'custommailer@appwrite.io', 'senderName' => 'Custom Mailer', 'replyTo' => 'reply@appwrite.io', - 'host' => 'maildev', - 'port' => 1025, - 'username' => '', - 'password' => '', + 'host' => $smtpHost, + 'port' => $smtpPort, + 'username' => $smtpUsername, + 'password' => $smtpPassword, ]); $this->assertEquals(204, $response['headers']['status-code']); @@ -752,10 +785,10 @@ class ProjectsConsoleClientTest extends Scope 'senderEmail' => 'custommailer@appwrite.io', 'senderName' => 'Custom Mailer', 'replyTo' => 'reply@appwrite.io', - 'host' => 'maildev', - 'port' => 1025, - 'username' => '', - 'password' => '', + 'host' => $smtpHost, + 'port' => $smtpPort, + 'username' => $smtpUsername, + 'password' => $smtpPassword, ]); $this->assertEquals(400, $response['headers']['status-code']); @@ -776,7 +809,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('Account Verification', $response['body']['subject']); + $this->assertEquals('Account Verification for {{project}}', $response['body']['subject']); $this->assertEquals('', $response['body']['senderEmail']); $this->assertEquals('verification', $response['body']['type']); $this->assertEquals('en-us', $response['body']['locale']); @@ -879,7 +912,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); - $this->assertEquals('Project Test 2', $response['body']['name']); + $this->assertEquals('Project Test', $response['body']['name']); $this->assertArrayHasKey('platforms', $response['body']); $this->assertArrayHasKey('webhooks', $response['body']); $this->assertArrayHasKey('keys', $response['body']); @@ -959,39 +992,6 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(15, $response['body']['authDuration']); - // Create session - $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ]), [ - 'email' => $userEmail, - 'password' => 'password', - ]); - - $this->assertEquals(201, $response['headers']['status-code']); - - $sessionCookie = $response['headers']['set-cookie']; - - // Wait 10 seconds, ensure valid session, extend session - \sleep(10); - - $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'Cookie' => $sessionCookie, - ])); - - $this->assertEquals(200, $response['headers']['status-code']); - - $response = $this->client->call(Client::METHOD_PATCH, '/account/sessions/current', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'cookie' => $sessionCookie, - ])); - - $this->assertEquals(200, $response['headers']['status-code']); - // Wait 20 seconds, ensure non-valid session \sleep(20); @@ -3132,7 +3132,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertContains('users.write', $response['body']['scopes']); $this->assertContains('collections.read', $response['body']['scopes']); $this->assertContains('tables.read', $response['body']['scopes']); - $this->assertCount(3, $response['body']['scopes']); + $this->assertCount(4, $response['body']['scopes']); $this->assertArrayHasKey('sdks', $response['body']); $this->assertEmpty($response['body']['sdks']); $this->assertArrayHasKey('accessedAt', $response['body']); @@ -3151,7 +3151,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertContains('users.write', $response['body']['scopes']); $this->assertContains('collections.read', $response['body']['scopes']); $this->assertContains('tables.read', $response['body']['scopes']); - $this->assertCount(3, $response['body']['scopes']); + $this->assertCount(4, $response['body']['scopes']); $this->assertArrayHasKey('sdks', $response['body']); $this->assertEmpty($response['body']['sdks']); $this->assertArrayHasKey('accessedAt', $response['body']); @@ -5086,8 +5086,8 @@ class ProjectsConsoleClientTest extends Scope $this->assertEmpty($response['body']); /** - * Get rate limit trying to use the deleted key - */ + * Get rate limit trying to use the deleted key + */ $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId,