diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 8f46ca3c62..b6e6337a00 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2371,7 +2371,18 @@ App::post('/v1/account/tokens/phone') $dbForProject->purgeCachedDocument('users', $user->getId()); } - $secret = Auth::codeGenerator(); + $secret = null; + $sendSMS = true; + $mockNumbers = $project->getAttribute('auths', [])['mockNumbers'] ?? []; + foreach ($mockNumbers as $mockNumber) { + if ($mockNumber['phone'] === $phone) { + $secret = $mockNumber['otp']; + $sendSMS = false; + break; + } + } + + $secret ??= Auth::codeGenerator(); $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_OTP)); $token = new Document([ @@ -2396,35 +2407,37 @@ App::post('/v1/account/tokens/phone') $dbForProject->purgeCachedDocument('users', $user->getId()); - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); + if ($sendSMS) { + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); - $customTemplate = $project->getAttribute('templates', [])['sms.login-' . $locale->default] ?? []; - if (!empty($customTemplate)) { - $message = $customTemplate['message'] ?? $message; + $customTemplate = $project->getAttribute('templates', [])['sms.login-' . $locale->default] ?? []; + if (!empty($customTemplate)) { + $message = $customTemplate['message'] ?? $message; + } + + $messageContent = Template::fromString($locale->getText("sms.verification.body")); + $messageContent + ->setParam('{{project}}', $project->getAttribute('name')) + ->setParam('{{secret}}', $secret); + $messageContent = \strip_tags($messageContent->render()); + $message = $message->setParam('{{token}}', $messageContent); + + $message = $message->render(); + + $messageDoc = new Document([ + '$id' => $token->getId(), + 'data' => [ + 'content' => $message, + ], + ]); + + $queueForMessaging + ->setType(MESSAGE_SEND_TYPE_INTERNAL) + ->setMessage($messageDoc) + ->setRecipients([$phone]) + ->setProviderType(MESSAGE_TYPE_SMS); } - $messageContent = Template::fromString($locale->getText("sms.verification.body")); - $messageContent - ->setParam('{{project}}', $project->getAttribute('name')) - ->setParam('{{secret}}', $secret); - $messageContent = \strip_tags($messageContent->render()); - $message = $message->setParam('{{token}}', $messageContent); - - $message = $message->render(); - - $messageDoc = new Document([ - '$id' => $token->getId(), - 'data' => [ - 'content' => $message, - ], - ]); - - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage($messageDoc) - ->setRecipients([$phone]) - ->setProviderType(MESSAGE_TYPE_SMS); - // Set to unhashed secret for events and server responses $token->setAttribute('secret', $secret); @@ -3439,7 +3452,8 @@ App::post('/v1/account/verification/phone') throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } - if (empty($user->getAttribute('phone'))) { + $phone = $user->getAttribute('phone'); + if (empty($phone)) { throw new Exception(Exception::USER_PHONE_NOT_FOUND); } @@ -3450,7 +3464,19 @@ App::post('/v1/account/verification/phone') $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); - $secret = Auth::codeGenerator(); + + $secret = null; + $sendSMS = true; + $mockNumbers = $project->getAttribute('auths', [])['mockNumbers'] ?? []; + foreach ($mockNumbers as $mockNumber) { + if ($mockNumber['phone'] === $phone) { + $secret = $mockNumber['otp']; + $sendSMS = false; + break; + } + } + + $secret ??= Auth::codeGenerator(); $expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM); $verification = new Document([ @@ -3475,35 +3501,37 @@ App::post('/v1/account/verification/phone') $dbForProject->purgeCachedDocument('users', $user->getId()); - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); + if ($sendSMS) { + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); - $customTemplate = $project->getAttribute('templates', [])['sms.verification-' . $locale->default] ?? []; - if (!empty($customTemplate)) { - $message = $customTemplate['message'] ?? $message; + $customTemplate = $project->getAttribute('templates', [])['sms.verification-' . $locale->default] ?? []; + if (!empty($customTemplate)) { + $message = $customTemplate['message'] ?? $message; + } + + $messageContent = Template::fromString($locale->getText("sms.verification.body")); + $messageContent + ->setParam('{{project}}', $project->getAttribute('name')) + ->setParam('{{secret}}', $secret); + $messageContent = \strip_tags($messageContent->render()); + $message = $message->setParam('{{token}}', $messageContent); + + $message = $message->render(); + + $messageDoc = new Document([ + '$id' => $verification->getId(), + 'data' => [ + 'content' => $message, + ], + ]); + + $queueForMessaging + ->setType(MESSAGE_SEND_TYPE_INTERNAL) + ->setMessage($messageDoc) + ->setRecipients([$user->getAttribute('phone')]) + ->setProviderType(MESSAGE_TYPE_SMS); } - $messageContent = Template::fromString($locale->getText("sms.verification.body")); - $messageContent - ->setParam('{{project}}', $project->getAttribute('name')) - ->setParam('{{secret}}', $secret); - $messageContent = \strip_tags($messageContent->render()); - $message = $message->setParam('{{token}}', $messageContent); - - $message = $message->render(); - - $messageDoc = new Document([ - '$id' => $verification->getId(), - 'data' => [ - 'content' => $message, - ], - ]); - - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage($messageDoc) - ->setRecipients([$user->getAttribute('phone')]) - ->setProviderType(MESSAGE_TYPE_SMS); - // Set to unhashed secret for events and server responses $verification->setAttribute('secret', $secret); diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index ff22337481..265efbbfae 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -2,6 +2,7 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\Validator\MockNumber; use Appwrite\Event\Delete; use Appwrite\Event\Mail; use Appwrite\Event\Validator\Event; @@ -105,6 +106,7 @@ App::post('/v1/projects') 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false, + 'mockNumbers' => [], 'sessionAlerts' => false, ]; @@ -856,6 +858,37 @@ App::patch('/v1/projects/:projectId/auth/max-sessions') $response->dynamic($project, Response::MODEL_PROJECT); }); +App::patch('/v1/projects/:projectId/auth/mock-numbers') + ->desc('Update the mock numbers for the project') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'projects') + ->label('sdk.method', 'updateMockNumbers') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROJECT) + ->param('projectId', '', new UID(), 'Project unique ID.') + ->param('numbers', '', new ArrayList(new MockNumber(), 10), 'An array of mock numbers and their corresponding verification codes (OTPs). Each number should be a valid E.164 formatted phone number. Maximum of 10 numbers are allowed.') + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $projectId, array $numbers, Response $response, Database $dbForConsole) { + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $auths = $project->getAttribute('auths', []); + + $auths['mockNumbers'] = $numbers; + + $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('auths', $auths)); + + $response->dynamic($project, Response::MODEL_PROJECT); + }); + App::delete('/v1/projects/:projectId') ->desc('Delete project') ->groups(['api', 'projects']) diff --git a/app/init.php b/app/init.php index fe28407724..a935687a93 100644 --- a/app/init.php +++ b/app/init.php @@ -777,6 +777,7 @@ $register->set('logger', function () { $adapter = new $classname($providerConfig); return new Logger($adapter); }); + $register->set('pools', function () { $group = new Group(); @@ -1320,6 +1321,7 @@ App::setResource('console', function () { 'legalAddress' => '', 'legalTaxId' => '', 'auths' => [ + 'mockNumbers' => [], 'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled', 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds diff --git a/src/Appwrite/Auth/Validator/MockNumber.php b/src/Appwrite/Auth/Validator/MockNumber.php new file mode 100644 index 0000000000..d94eb83f23 --- /dev/null +++ b/src/Appwrite/Auth/Validator/MockNumber.php @@ -0,0 +1,81 @@ +message; + } + + /** + * Is valid. + * + * @param mixed $value + * + * @return bool + */ + public function isValid($value): bool + { + if (!\is_array($value) || !isset($value['phone']) || !isset($value['otp'])) { + $this->message = 'Invalid payload structure. Please check the "phone" and "otp" fields'; + return false; + } + + $phone = new Phone(); + if (!$phone->isValid($value['phone'])) { + $this->message = $phone->getDescription(); + return false; + } + + $otp = new Text(6, 6); + if (!$otp->isValid($value['otp'])) { + $this->message = 'OTP must be a valid string and exactly 6 characters.'; + return false; + } + + return true; + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +} diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index b7a2133484..1e1d5f87ae 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -72,6 +72,7 @@ use Appwrite\Utopia\Response\Model\Migration; use Appwrite\Utopia\Response\Model\MigrationFirebaseProject; use Appwrite\Utopia\Response\Model\MigrationReport; use Appwrite\Utopia\Response\Model\Mock; +use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; use Appwrite\Utopia\Response\Model\Phone; use Appwrite\Utopia\Response\Model\Platform; @@ -269,6 +270,8 @@ class Response extends SwooleResponse public const MODEL_WEBHOOK_LIST = 'webhookList'; public const MODEL_KEY = 'key'; public const MODEL_KEY_LIST = 'keyList'; + public const MODEL_MOCK_NUMBER = 'mockNumber'; + public const MODEL_MOCK_NUMBER_LIST = 'mockNumberList'; public const MODEL_AUTH_PROVIDER = 'authProvider'; public const MODEL_AUTH_PROVIDER_LIST = 'authProviderList'; public const MODEL_PLATFORM = 'platform'; @@ -348,6 +351,7 @@ class Response extends SwooleResponse ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) ->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false)) + ->setModel(new BaseList('Mock Numbers List', self::MODEL_MOCK_NUMBER_LIST, 'numbers', self::MODEL_MOCK_NUMBER, true, false)) ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) @@ -419,6 +423,7 @@ class Response extends SwooleResponse ->setModel(new Project()) ->setModel(new Webhook()) ->setModel(new Key()) + ->setModel(new MockNumber()) ->setModel(new AuthProvider()) ->setModel(new Platform()) ->setModel(new Variable()) diff --git a/src/Appwrite/Utopia/Response/Model/MockNumber.php b/src/Appwrite/Utopia/Response/Model/MockNumber.php new file mode 100644 index 0000000000..14ce747da6 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/MockNumber.php @@ -0,0 +1,47 @@ +addRule('phone', [ + 'type' => self::TYPE_STRING, + 'description' => 'Mock phone number for testing phone authentication. Useful for testing phone authentication without sending an SMS.', + 'default' => '', + 'example' => '+1612842323', + ]) + ->addRule('otp', [ + 'type' => self::TYPE_STRING, + 'description' => 'Mock OTP for the number. ', + 'default' => '', + 'example' => '123456', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Mock Number'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_MOCK_NUMBER; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index da97ba2c19..1a8bee3f3d 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -138,6 +138,12 @@ class Project extends Model 'default' => false, 'example' => true, ]) + ->addRule('authMockNumbers', [ + 'type' => Response::MODEL_MOCK_NUMBER_LIST, + 'description' => 'An array of mock numbers and their corresponding verification codes (OTPs).', + 'default' => [], + 'example' => true, + ]) ->addRule('authSessionAlerts', [ 'type' => self::TYPE_BOOLEAN, 'description' => 'Whether or not to send session alert emails to users.', @@ -327,6 +333,7 @@ class Project extends Model $document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0); $document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false); $document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false); + $document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []); $document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false); foreach ($auth as $index => $method) { diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 18934f5125..753322f168 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -610,7 +610,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'emails' => [ 'testuser@appwrite.io', 'testusertwo@appwrite.io' ], + 'emails' => ['testuser@appwrite.io', 'testusertwo@appwrite.io'], 'senderEmail' => 'custommailer@appwrite.io', 'senderName' => 'Custom Mailer', 'replyTo' => 'reply@appwrite.io', @@ -647,7 +647,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'emails' => [ 'u1@appwrite.io', 'u2@appwrite.io', 'u3@appwrite.io', 'u4@appwrite.io', 'u5@appwrite.io', 'u6@appwrite.io', 'u7@appwrite.io', 'u8@appwrite.io', 'u9@appwrite.io', 'u10@appwrite.io' ], + 'emails' => ['u1@appwrite.io', 'u2@appwrite.io', 'u3@appwrite.io', 'u4@appwrite.io', 'u5@appwrite.io', 'u6@appwrite.io', 'u7@appwrite.io', 'u8@appwrite.io', 'u9@appwrite.io', 'u10@appwrite.io'], 'senderEmail' => 'custommailer@appwrite.io', 'senderName' => 'Custom Mailer', 'replyTo' => 'reply@appwrite.io', @@ -663,7 +663,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'emails' => [ 'u1@appwrite.io', 'u2@appwrite.io', 'u3@appwrite.io', 'u4@appwrite.io', 'u5@appwrite.io', 'u6@appwrite.io', 'u7@appwrite.io', 'u8@appwrite.io', 'u9@appwrite.io', 'u10@appwrite.io', 'u11@appwrite.io' ], + 'emails' => ['u1@appwrite.io', 'u2@appwrite.io', 'u3@appwrite.io', 'u4@appwrite.io', 'u5@appwrite.io', 'u6@appwrite.io', 'u7@appwrite.io', 'u8@appwrite.io', 'u9@appwrite.io', 'u10@appwrite.io', 'u11@appwrite.io'], 'senderEmail' => 'custommailer@appwrite.io', 'senderName' => 'Custom Mailer', 'replyTo' => 'reply@appwrite.io', @@ -1529,8 +1529,8 @@ class ProjectsConsoleClientTest extends Scope /** - * Reset - */ + * Reset + */ $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-history', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1543,6 +1543,178 @@ class ProjectsConsoleClientTest extends Scope return $data; } + /** + * @group smtpAndTemplates + * @group projectsCRUD + * + * @depends testCreateProject + * */ + public function testUpdateMockNumbers($data) + { + $id = $data['projectId'] ?? ''; + + /** + * Test for Failure + */ + + /** Trying to pass an empty body to the endpoint */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('Param "numbers" is not optional.', $response['body']['message']); + + /** Trying to pass body with incorrect structure */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'numbers' => [ + 'phone' => '+1655513432', + 'otp' => '123456' + ] + ]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and Invalid payload structure. Please check the "phone" and "otp" fields', $response['body']['message']); + + /** Trying to pass an OTP longer than 6 characters*/ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'numbers' => [ + [ + 'phone' => '+1655513432', + 'otp' => '12345678' + ] + ] + ]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and OTP must be a valid string and exactly 6 characters.', $response['body']['message']); + + /** Trying to pass an OTP shorter than 6 characters*/ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'numbers' => [ + [ + 'phone' => '+1655513432', + 'otp' => '123' + ] + ] + ]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and OTP must be a valid string and exactly 6 characters.', $response['body']['message']); + + /** Trying to pass an invalid phone number */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'numbers' => [ + [ + 'phone' => '1655234', + 'otp' => '123456' + ] + ] + ]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and Phone number must start with a \'+\' can have a maximum of fifteen digits.', $response['body']['message']); + + /** Trying to pass a number longer than 15 digits */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'numbers' => [ + [ + 'phone' => '+1234567890987654', + 'otp' => '123456' + ] + ] + ]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and Phone number must start with a \'+\' can have a maximum of fifteen digits.', $response['body']['message']); + + $numbers = []; + for ($i = 0; $i < 11; $i++) { + $numbers[] = [ + 'phone' => '+1655513432', + 'otp' => '123456' + ]; + } + + /** Trying to pass more than 10 values */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'numbers' => $numbers + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and Phone number must start with a \'+\' can have a maximum of fifteen digits.', $response['body']['message']); + + /** + * Test for success + */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'numbers' => [] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'numbers' => [ + [ + 'phone' => '+1655513432', + 'otp' => '123456' + ] + ] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + + // Create phone session for this project and check if the mock number is used + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + ]), [ + 'userId' => 'unique()', + 'phone' => '+1655513432', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $userId = $response['body']['userId']; + + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + ]), [ + 'userId' => $userId, + 'secret' => '654321', // Try a random code + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + ]), [ + 'userId' => $userId, + 'secret' => '123456', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + } + /** * @depends testUpdateProjectAuthLimit */ @@ -1646,8 +1818,8 @@ class ProjectsConsoleClientTest extends Scope /** - * Reset - */ + * Reset + */ $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-history', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'],