From dc258836858ccc8b9105af8da822efffcb92f87e Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 8 Jun 2022 14:50:31 +0200 Subject: [PATCH] feat: add update and verification method for account and users --- app/config/collections.php | 2 +- app/controllers/api/account.php | 208 +++++++++++- app/controllers/api/users.php | 90 +++++ app/controllers/general.php | 3 +- src/Appwrite/Auth/Validator/Phone.php | 2 +- tests/e2e/Services/Account/AccountBase.php | 139 +------- .../Account/AccountCustomClientTest.php | 309 ++++++++++++++++++ tests/unit/Auth/Validator/PhoneTest.php | 2 + 8 files changed, 607 insertions(+), 148 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 035d7984d..96674a474 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -985,7 +985,7 @@ $collections = [ '$id' => 'phone', 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 320, + 'size' => 16, // leading '+' and 15 digitts maximum by E.164 format 'signed' => true, 'required' => false, 'default' => null, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index a5d94cdb6..27d7db7d7 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1519,18 +1519,15 @@ App::patch('/v1/account/email') } $email = \strtolower($email); - $profile = $dbForProject->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address - if ($profile) { - throw new Exception('User already registered', 409, Exception::USER_ALREADY_EXISTS); - } + $user + ->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', '')) + ->setAttribute('email', $email) + ->setAttribute('emailVerification', false) // After this user needs to confirm mail again + ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')])); try { - $user = $dbForProject->updateDocument('users', $user->getId(), $user - ->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', '')) - ->setAttribute('email', $email) - ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')]))); + $user = $dbForProject->updateDocument('users', $user->getId(), $user); } catch (Duplicate $th) { throw new Exception('Email already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -1546,6 +1543,59 @@ App::patch('/v1/account/email') $response->dynamic($user, Response::MODEL_USER); }); +App::patch('/v1/account/phone') + ->desc('Update Account Phone') + ->groups(['api', 'account']) + ->label('event', 'users.[userId].update.phone') + ->label('scope', 'account') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'updatePhone') + ->label('sdk.description', '/docs/references/account/update-phone.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('number', '', new ValidatorPhone(), 'Phone number.') + ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') + ->inject('response') + ->inject('user') + ->inject('dbForProject') + ->inject('audits') + ->inject('usage') + ->inject('events') + ->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + + $isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting + + if ( + !$isAnonymousUser && + !Auth::passwordVerify($password, $user->getAttribute('password')) + ) { // Double check user password + throw new Exception('Invalid credentials', 401, Exception::USER_INVALID_CREDENTIALS); + } + + $user + ->setAttribute('phone', $phone) + ->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again + ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')])); + + try { + $user = $dbForProject->updateDocument('users', $user->getId(), $user); + } catch (Duplicate $th) { + throw new Exception('Phone number already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS); + } + + $audits + ->setResource('user/' . $user->getId()) + ->setUser($user) + ; + + $usage->setParam('users.update', 1); + $events->setParam('userId', $user->getId()); + + $response->dynamic($user, Response::MODEL_USER); + }); + App::patch('/v1/account/prefs') ->desc('Update Account Preferences') ->groups(['api', 'account']) @@ -2172,3 +2222,143 @@ App::put('/v1/account/verification') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); + +App::post('/v1/account/verification/phone') + ->desc('Create Phone Verification') + ->groups(['api', 'account']) + ->label('scope', 'account') + ->label('event', 'users.[userId].verification.[tokenId].create') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createPhoneVerification') + ->label('sdk.description', '/docs/references/account/create-phone-verification.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TOKEN) + ->label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}') + ->inject('request') + ->inject('response') + ->inject('phone') + ->inject('user') + ->inject('dbForProject') + ->inject('audits') + ->inject('events') + ->inject('usage') + ->action(function (Request $request, Response $response, Phone $phone, Document $user, Database $dbForProject, Audit $audits, Event $events, Stats $usage) { + + if (empty(App::getEnv('_APP_SMTP_HOST'))) { + throw new Exception('SMTP Disabled', 503, Exception::GENERAL_SMTP_DISABLED); + } + + if (empty($user->getAttribute('phone'))) { + throw new Exception('User has no phone number.', 400); + } + + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + + $verificationSecret = Auth::tokenGenerator(); + + $secret = $phone->generateSecretDigits(); + $expire = \time() + Auth::TOKEN_EXPIRATION_CONFIRM; + + $verification = new Document([ + '$id' => $dbForProject->getId(), + 'userId' => $user->getId(), + 'type' => Auth::TOKEN_TYPE_PHONE, + 'secret' => $secret, + 'expire' => $expire, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + ]); + + Authorization::setRole('user:' . $user->getId()); + + $verification = $dbForProject->createDocument('tokens', $verification + ->setAttribute('$read', ['user:' . $user->getId()]) + ->setAttribute('$write', ['user:' . $user->getId()])); + + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $phone->send(APP::getEnv('_APP_PHONE_FROM'), $user->getAttribute('phone'), $secret); + + $events + ->setParam('userId', $user->getId()) + ->setParam('tokenId', $verification->getId()) + ->setPayload($response->output( + $verification->setAttribute('secret', $verificationSecret), + Response::MODEL_TOKEN + )) + ; + + // Hide secret for clients + $verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : ''); + + $audits->setResource('user/' . $user->getId()); + $usage->setParam('users.update', 1); + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($verification, Response::MODEL_TOKEN); + }); + +App::put('/v1/account/verification/phone') + ->desc('Create Phone Verification (confirmation)') + ->groups(['api', 'account']) + ->label('scope', 'public') + ->label('event', 'users.[userId].verification.[tokenId].update') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'updatePhoneVerification') + ->label('sdk.description', '/docs/references/account/update-phone-verification.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TOKEN) + ->label('abuse-limit', 10) + ->label('abuse-key', 'userId:{param-userId}') + ->param('userId', '', new UID(), 'User ID.') + ->param('secret', '', new Text(256), 'Valid verification token.') + ->inject('response') + ->inject('user') + ->inject('dbForProject') + ->inject('audits') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + + $profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId)); + + if ($profile->isEmpty()) { + throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); + } + + $verification = Auth::phoneTokenVerify($user->getAttribute('tokens', []), $secret); + + if (!$verification) { + throw new Exception('Invalid verification token', 401, Exception::USER_INVALID_TOKEN); + } + + Authorization::setRole('user:' . $profile->getId()); + + $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true)); + + $verificationDocument = $dbForProject->getDocument('tokens', $verification); + + /** + * We act like we're updating and validating the verification token but actually we don't need it anymore. + */ + $dbForProject->deleteDocument('tokens', $verification); + $dbForProject->deleteCachedDocument('users', $profile->getId()); + + $audits->setResource('user/' . $user->getId()); + + $usage->setParam('users.update', 1); + + $events + ->setParam('userId', $user->getId()) + ->setParam('tokenId', $verificationDocument->getId()) + ; + + $response->dynamic($verificationDocument, Response::MODEL_TOKEN); + }); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index cada9e51d..c1682174c 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -2,6 +2,7 @@ use Appwrite\Auth\Auth; use Appwrite\Auth\Validator\Password; +use Appwrite\Auth\Validator\Phone; use Appwrite\Detector\Detector; use Appwrite\Event\Delete; use Appwrite\Event\Event; @@ -438,6 +439,45 @@ App::patch('/v1/users/:userId/verification') $response->dynamic($user, Response::MODEL_USER); }); +App::patch('/v1/users/:userId/verification') + ->desc('Update Phone Verification') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].update.verification') + ->label('scope', 'users.write') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'updatePhoneVerification') + ->label('sdk.description', '/docs/references/users/update-user-phone-verification.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new UID(), 'User ID.') + ->param('phoneVerification', false, new Boolean(), 'User phone verification status.') + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, bool $phoneVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); + } + + $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification)); + + $usage + ->setParam('users.update', 1) + ; + + $events + ->setParam('userId', $user->getId()) + ; + + $response->dynamic($user, Response::MODEL_USER); + }); + App::patch('/v1/users/:userId/name') ->desc('Update Name') ->groups(['api', 'users']) @@ -555,6 +595,7 @@ App::patch('/v1/users/:userId/email') $user ->setAttribute('email', $email) + ->setAttribute('emailVerification', false) ->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name')])) ; @@ -565,6 +606,55 @@ App::patch('/v1/users/:userId/email') } + $audits + ->setResource('user/' . $user->getId()) + ; + + $events + ->setParam('userId', $user->getId()) + ; + + $response->dynamic($user, Response::MODEL_USER); + }); + +App::patch('/v1/users/:userId/email') + ->desc('Update Phone') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].update.phone') + ->label('scope', 'users.write') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'updatePhone') + ->label('sdk.description', '/docs/references/users/update-user-phone.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new UID(), 'User ID.') + ->param('number', '', new Phone(), 'User phone number.') + ->inject('response') + ->inject('dbForProject') + ->inject('audits') + ->inject('events') + ->action(function (string $userId, string $number, Response $response, Database $dbForProject, EventAudit $audits, Event $events) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); + } + + $user + ->setAttribute('phone', $number) + ->setAttribute('phoneVerification', false) + ; + + try { + $user = $dbForProject->updateDocument('users', $user->getId(), $user); + } catch (Duplicate $th) { + throw new Exception('Email already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS); + } + + $audits ->setResource('user/' . $user->getId()) ; diff --git a/app/controllers/general.php b/app/controllers/general.php index a67d9cfd7..bd05fea04 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -298,11 +298,10 @@ App::init(function (App $utopia, Request $request, Response $response, Document $service = $route->getLabel('sdk.namespace', ''); if (!empty($service)) { - $roles = Authorization::getRoles(); if ( array_key_exists($service, $project->getAttribute('services', [])) && !$project->getAttribute('services', [])[$service] - && !(Auth::isPrivilegedUser($roles) || Auth::isAppUser($roles)) + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) ) { throw new AppwriteException('Service is disabled', 503, AppwriteException::GENERAL_SERVICE_DISABLED); } diff --git a/src/Appwrite/Auth/Validator/Phone.php b/src/Appwrite/Auth/Validator/Phone.php index 0852a0cb6..32c3ca339 100644 --- a/src/Appwrite/Auth/Validator/Phone.php +++ b/src/Appwrite/Auth/Validator/Phone.php @@ -32,7 +32,7 @@ class Phone extends Validator */ public function isValid($value): bool { - return !!\preg_match('/^\+[1-9]\d{1,14}$/', $value); + return is_string($value) && !!\preg_match('/^\+[1-9]\d{1,14}$/', $value); } /** diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index b65326c5c..71047e109 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -2,7 +2,6 @@ namespace Tests\E2E\Services\Account; -use Appwrite\Auth\Phone\Mock; use Tests\E2E\Client; trait AccountBase @@ -484,8 +483,7 @@ trait AccountBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - ]); + ]), []); $this->assertEquals($response['headers']['status-code'], 400); @@ -560,8 +558,7 @@ trait AccountBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - ]); + ]), []); $this->assertEquals($response['headers']['status-code'], 400); @@ -641,8 +638,7 @@ trait AccountBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - ]); + ]), []); $this->assertEquals($response['headers']['status-code'], 400); @@ -1330,132 +1326,6 @@ trait AccountBase return $data; } - public function testCreatePhone(): array - { - $number = '+1 234 56789'; - - /** - * Test for SUCCESS - */ - $response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]), [ - 'userId' => 'unique()', - 'number' => $number, - ]); - - $this->assertEquals(201, $response['headers']['status-code']); - $this->assertNotEmpty($response['body']['$id']); - $this->assertEmpty($response['body']['secret']); - $this->assertIsNumeric($response['body']['expire']); - - $userId = $response['body']['userId']; - - /** - * Test for FAILURE - */ - $response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]), [ - 'userId' => 'unique()' - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - $data['token'] = Mock::$defaultDigits; - $data['id'] = $userId; - $data['number'] = $number; - - return $data; - } - - /** - * @depends testCreatePhone - */ - public function testCreateSessionWithPhone($data): void - { - $id = $data['id'] ?? ''; - $token = $data['token'] ?? ''; - $number = $data['number'] ?? ''; - - /** - * Test for FAILURE - */ - $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]), [ - 'userId' => 'ewewe', - 'secret' => $token, - ]); - - $this->assertEquals(404, $response['headers']['status-code']); - - $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]), [ - 'userId' => $id, - 'secret' => 'sdasdasdasd', - ]); - - $this->assertEquals(401, $response['headers']['status-code']); - - /** - * Test for SUCCESS - */ - $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]), [ - 'userId' => $id, - 'secret' => $token, - ]); - - $this->assertEquals(201, $response['headers']['status-code']); - $this->assertIsArray($response['body']); - $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']['$id']); - $this->assertNotEmpty($response['body']['userId']); - - $session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']]; - - $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($response['headers']['status-code'], 200); - $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']['$id']); - $this->assertIsNumeric($response['body']['registration']); - $this->assertEquals($response['body']['phone'], $number); - $this->assertTrue($response['body']['phoneVerification']); - - /** - * Test for FAILURE - */ - $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]), [ - 'userId' => $id, - 'secret' => $token, - ]); - - $this->assertEquals(401, $response['headers']['status-code']); - } - /** * @depends testCreateMagicUrl */ @@ -1586,8 +1456,7 @@ trait AccountBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - ]); + ]), []); $this->assertEquals($response['headers']['status-code'], 400); diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index b54711305..a6ffd8faf 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -2,6 +2,7 @@ namespace Tests\E2E\Services\Account; +use Appwrite\Auth\Phone\Mock; use Tests\E2E\Client; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\ProjectCustom; @@ -673,4 +674,312 @@ class AccountCustomClientTest extends Scope $this->assertCount(1, $response['body']['users']); $this->assertEquals($response['body']['users'][0]['email'], $email); } + + + public function testCreatePhone(): array + { + $number = '+123456789'; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => 'unique()', + 'number' => $number, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEmpty($response['body']['secret']); + $this->assertIsNumeric($response['body']['expire']); + + $userId = $response['body']['userId']; + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => 'unique()' + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $data['token'] = Mock::$defaultDigits; + $data['id'] = $userId; + $data['number'] = $number; + + return $data; + } + + /** + * @depends testCreatePhone + */ + public function testCreateSessionWithPhone(array $data): array + { + $id = $data['id'] ?? ''; + $token = $data['token'] ?? ''; + $number = $data['number'] ?? ''; + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => 'ewewe', + 'secret' => $token, + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => $id, + 'secret' => 'sdasdasdasd', + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => $id, + 'secret' => $token, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertIsArray($response['body']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertNotEmpty($response['body']['userId']); + + $session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']]; + + $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($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertIsNumeric($response['body']['registration']); + $this->assertEquals($response['body']['phone'], $number); + $this->assertTrue($response['body']['phoneVerification']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => $id, + 'secret' => $token, + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + $data['session'] = $session; + + return $data; + } + + /** + * @depends testCreateSessionWithPhone + */ + public function testConvertPhoneToPassword(array $data): array + { + $session = $data['session']; + $email = uniqid() . 'new@localhost.test'; + $password = 'new-password'; + + /** + * Test for SUCCESS + */ + $email = uniqid() . 'new@localhost.test'; + + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertIsArray($response['body']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertIsNumeric($response['body']['registration']); + $this->assertEquals($response['body']['email'], $email); + + $response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 201); + + return $data; + } + + /** + * @depends testConvertPhoneToPassword + */ + public function testUpdatePhone(array $data): array + { + $newPhone = '+45632569856'; + $session = $data['session'] ?? ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'number' => $newPhone, + 'password' => 'new-password' + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertIsArray($response['body']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertIsNumeric($response['body']['registration']); + $this->assertEquals($response['body']['phone'], $newPhone); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals($response['headers']['status-code'], 401); + + $response = $this->client->call(Client::METHOD_PATCH, '/account/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), []); + + $this->assertEquals($response['headers']['status-code'], 400); + + $data['phone'] = $newPhone; + + return $data; + } + + /** + * @depends testUpdatePhone + */ + public function testPhoneVerification(array $data): array + { + $session = $data['session'] ?? ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/account/verification/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + + ])); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEmpty($response['body']['secret']); + $this->assertIsNumeric($response['body']['expire']); + + return $data; + } + + /** + * @depends testPhoneVerification + */ + public function testUpdatePhoneVerification($data): array + { + $id = $data['id'] ?? ''; + $session = $data['session'] ?? ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PUT, '/account/verification/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'userId' => $id, + 'secret' => Mock::$defaultDigits, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PUT, '/account/verification/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'userId' => 'ewewe', + 'secret' => Mock::$defaultDigits, + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/account/verification/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'userId' => $id, + 'secret' => '999999', + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + return $data; + } } diff --git a/tests/unit/Auth/Validator/PhoneTest.php b/tests/unit/Auth/Validator/PhoneTest.php index 39b6c9857..f6695918c 100644 --- a/tests/unit/Auth/Validator/PhoneTest.php +++ b/tests/unit/Auth/Validator/PhoneTest.php @@ -28,6 +28,8 @@ class PhoneTest extends TestCase $this->assertEquals($this->object->isValid('786-307-3615'), false); $this->assertEquals($this->object->isValid('+16308A520397'), false); $this->assertEquals($this->object->isValid('+0415553452342'), false); + $this->assertEquals($this->object->isValid('+14 155 5524564'), false); + $this->assertEquals($this->object->isValid(+14155552456), false); $this->assertEquals($this->object->isValid('+14155552'), true); $this->assertEquals($this->object->isValid('+141555526'), true);