diff --git a/app/config/errors.php b/app/config/errors.php index baff8b5937..7a23b60e74 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -178,7 +178,7 @@ return [ ], Exception::USER_SESSION_ALREADY_EXISTS => [ 'name' => Exception::USER_SESSION_ALREADY_EXISTS, - 'description' => 'Creation of anonymous users is prohibited when a session is active.', + 'description' => 'Creation of a session is prohibited when a session is active.', 'code' => 401, ], Exception::USER_NOT_FOUND => [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 835d35f81c..6c748bec5a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -228,7 +228,6 @@ App::post('/v1/account/sessions/email') ->inject('queueForEvents') ->inject('hooks') ->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Hooks $hooks) { - $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -553,7 +552,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('geodb') ->inject('queueForEvents') ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents) use ($oauthDefaultSuccess) { - $protocol = $request->getProtocol(); $callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId(); $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; @@ -675,6 +673,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if (!empty($userWithMatchingEmail)) { throw new Exception(Exception::USER_ALREADY_EXISTS); } + + $sessionUpgrade = true; } $sessions = $user->getAttribute('sessions', []); @@ -704,7 +704,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } /** - * Is verified is not used yet, since we don't know after an accout is created anymore if it was verified or not. + * 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); @@ -947,6 +947,20 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } + if (isset($sessionUpgrade) && $sessionUpgrade) { + foreach ($user->getAttribute('targets', []) as $target) { + if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { + continue; + } + + $target + ->setAttribute('sessionId', $session->getId()) + ->setAttrubte('sessionInternalId', $session->getInternalId()); + + $dbForProject->updateDocument('targets', $target->getId(), $target); + } + } + $dbForProject->purgeCachedDocument('users', $user->getId()); $state['success']['query'] = URLParser::unparseQuery($query); @@ -1636,7 +1650,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res App::put('/v1/account/sessions/magic-url') ->desc('Update magic URL session') ->label('event', 'users.[userId].sessions.[sessionId].create') - ->groups(['api', 'account']) + ->groups(['api', 'account', 'session']) ->label('scope', 'sessions.write') ->label('audits.event', 'session.create') ->label('audits.resource', 'user/{response.userId}') @@ -1666,7 +1680,7 @@ App::put('/v1/account/sessions/magic-url') App::put('/v1/account/sessions/phone') ->desc('Update phone session') ->label('event', 'users.[userId].sessions.[sessionId].create') - ->groups(['api', 'account']) + ->groups(['api', 'account', 'session']) ->label('scope', 'sessions.write') ->label('audits.event', 'session.create') ->label('audits.resource', 'user/{response.userId}') @@ -1696,7 +1710,7 @@ App::put('/v1/account/sessions/phone') App::post('/v1/account/sessions/token') ->desc('Create session') ->label('event', 'users.[userId].sessions.[sessionId].create') - ->groups(['api', 'account']) + ->groups(['api', 'account', 'session']) ->label('scope', 'sessions.write') ->label('audits.event', 'session.create') ->label('audits.resource', 'user/{response.userId}') @@ -1919,7 +1933,6 @@ App::post('/v1/account/sessions/anonymous') ->inject('geodb') ->inject('queueForEvents') ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents) { - $protocol = $request->getProtocol(); $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); @@ -1929,10 +1942,6 @@ App::post('/v1/account/sessions/anonymous') throw new Exception(Exception::USER_ANONYMOUS_CONSOLE_PROHIBITED, 'Failed to create anonymous user'); } - if (!$user->isEmpty()) { - throw new Exception(Exception::USER_SESSION_ALREADY_EXISTS, 'Cannot create an anonymous user when logged in'); - } - $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if ($limit !== 0) { diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index e75161d035..0101d72116 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -461,6 +461,20 @@ App::init() } }); +App::init() + ->groups(['session']) + ->inject('user') + ->inject('request') + ->action(function (Document $user, Request $request) { + if (\str_contains($request->getURI(), 'oauth2')) { + return; + } + + if (!$user->isEmpty()) { + throw new Exception(Exception::USER_SESSION_ALREADY_EXISTS); + } + }); + /** * Limit user session * @@ -497,6 +511,7 @@ App::shutdown() $session = array_shift($sessions); $dbForProject->deleteDocument('sessions', $session->getId()); } + $dbForProject->purgeCachedDocument('users', $userId); }); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 9c66e72821..7b34feb8b1 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -676,7 +676,7 @@ services: - _APP_DB_PASS appwrite-assistant: - image: appwrite/assistant:0.3.0 + image: appwrite/assistant:0.4.0 container_name: appwrite-assistant <<: *x-logging restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml index 3d96b5059c..f29d13182a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -782,7 +782,7 @@ services: appwrite-assistant: container_name: appwrite-assistant - image: appwrite/assistant:0.3.0 + image: appwrite/assistant:0.4.0 networks: - appwrite environment: diff --git a/src/Appwrite/Migration/Version/V20.php b/src/Appwrite/Migration/Version/V20.php index 0ef899588e..30592e7d6a 100644 --- a/src/Appwrite/Migration/Version/V20.php +++ b/src/Appwrite/Migration/Version/V20.php @@ -252,6 +252,14 @@ class V20 extends Migration Console::warning("'totpBackup' from {$id}: {$th->getMessage()}"); } + // Create challenges attribute + try { + $this->createAttributeFromCollection($this->projectDB, $id, 'challenges'); + $this->projectDB->purgeCachedCollection($id); + } catch (Throwable $th) { + Console::warning("'challenges' from {$id}: {$th->getMessage()}"); + } + break; case 'projects': // Rename providers authProviders to oAuthProviders @@ -543,9 +551,11 @@ class V20 extends Migration $document->setAttribute('expire', $expire); $factors = match ($document->getAttribute('provider')) { - Auth::SESSION_PROVIDER_ANONYMOUS => ['anonymous'], + Auth::SESSION_PROVIDER_EMAIL => ['password'], Auth::SESSION_PROVIDER_PHONE => ['phone'], - default => ['password'], + Auth::SESSION_PROVIDER_ANONYMOUS => ['anonymous'], + Auth::SESSION_PROVIDER_TOKEN => ['token'], + default => ['email'], }; $document->setAttribute('factors', $factors); diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index aacd49f94a..ab3a9a41b1 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -86,7 +86,7 @@ class Messaging extends Action $payload = $message->getPayload() ?? []; if (empty($payload)) { - throw new Exception('Missing payload'); + throw new \Exception('Missing payload'); } $type = $payload['type'] ?? ''; @@ -105,7 +105,7 @@ class Messaging extends Action $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $deviceForLocalFiles,); break; default: - throw new Exception('Unknown message type: ' . $type); + throw new \Exception('Unknown message type: ' . $type); } } @@ -118,11 +118,12 @@ class Messaging extends Action $topicIds = $message->getAttribute('topics', []); $targetIds = $message->getAttribute('targets', []); $userIds = $message->getAttribute('users', []); + $providerType = $message->getAttribute('providerType'); /** - * @var array $recipients + * @var array $allTargets */ - $recipients = []; + $allTargets = []; if (\count($topicIds) > 0) { $topics = $dbForProject->find('topics', [ @@ -130,9 +131,11 @@ class Messaging extends Action Query::limit(\count($topicIds)), ]); foreach ($topics as $topic) { - $targets = \array_filter($topic->getAttribute('targets'), fn(Document $target) => - $target->getAttribute('providerType') === $message->getAttribute('providerType')); - $recipients = \array_merge($recipients, $targets); + $targets = \array_filter($topic->getAttribute('targets'), function (Document $target) use ($providerType) { + return $target->getAttribute('providerType') === $providerType; + }); + + \array_push($allTargets, ...$targets); } } @@ -142,23 +145,25 @@ class Messaging extends Action Query::limit(\count($userIds)), ]); foreach ($users as $user) { - $targets = \array_filter($user->getAttribute('targets'), fn(Document $target) => - $target->getAttribute('providerType') === $message->getAttribute('providerType')); - $recipients = \array_merge($recipients, $targets); + $targets = \array_filter($user->getAttribute('targets'), function (Document $target) use ($providerType) { + return $target->getAttribute('providerType') === $providerType; + }); + + \array_push($allTargets, ...$targets); } } if (\count($targetIds) > 0) { $targets = $dbForProject->find('targets', [ Query::equal('$id', $targetIds), + Query::equal('providerType', [$providerType]), Query::limit(\count($targetIds)), ]); - $targets = \array_filter($targets, fn(Document $target) => - $target->getAttribute('providerType') === $message->getAttribute('providerType')); - $recipients = \array_merge($recipients, $targets); + + \array_push($allTargets, ...$targets); } - if (empty($recipients)) { + if (empty($allTargets)) { $dbForProject->updateDocument('messages', $message->getId(), $message->setAttributes([ 'status' => MessageStatus::FAILED, 'deliveryErrors' => ['No valid recipients found.'] @@ -168,85 +173,82 @@ class Messaging extends Action return; } - $fallback = $dbForProject->findOne('providers', [ + $default = $dbForProject->findOne('providers', [ Query::equal('enabled', [true]), - Query::equal('type', [$recipients[0]->getAttribute('providerType')]), + Query::equal('type', [$providerType]), ]); - if ($fallback === false || $fallback->isEmpty()) { + if ($default === false || $default->isEmpty()) { $dbForProject->updateDocument('messages', $message->getId(), $message->setAttributes([ 'status' => MessageStatus::FAILED, - 'deliveryErrors' => ['No fallback provider found.'] + 'deliveryErrors' => ['No enabled provider found.'] ])); - Console::warning('No fallback provider found.'); + Console::warning('No enabled provider found.'); return; } /** - * @var array> $identifiers + * @var array> $identifiers */ $identifiers = []; /** - * @var Document[] $providers + * @var array $providers */ $providers = [ - $fallback->getId() => $fallback + $default->getId() => $default ]; - foreach ($recipients as $recipient) { - $providerId = $recipient->getAttribute('providerId'); + foreach ($allTargets as $target) { + $providerId = $target->getAttribute('providerId'); - if ( - !$providerId - && $fallback instanceof Document - && !$fallback->isEmpty() - && $fallback->getAttribute('enabled') - ) { - $providerId = $fallback->getId(); + if (!$providerId) { + $providerId = $default->getId(); } if ($providerId) { if (!\array_key_exists($providerId, $identifiers)) { $identifiers[$providerId] = []; } - $identifiers[$providerId][] = $recipient->getAttribute('identifier'); + // Use null as value to avoid duplicate keys + $identifiers[$providerId][$target->getAttribute('identifier')] = null; } } /** * @var array $results */ - $results = batch(\array_map(function ($providerId) use ($identifiers, $providers, $fallback, $message, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { - return function () use ($providerId, $identifiers, $providers, $fallback, $message, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { + $results = batch(\array_map(function ($providerId) use ($identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { + return function () use ($providerId, $identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { if (\array_key_exists($providerId, $providers)) { $provider = $providers[$providerId]; } else { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty() || !$provider->getAttribute('enabled')) { - $provider = $fallback; + $provider = $default; } else { $providers[$providerId] = $provider; } } - $identifiers = $identifiers[$providerId]; + $identifiersForProvider = $identifiers[$providerId]; $adapter = match ($provider->getAttribute('type')) { MESSAGE_TYPE_SMS => $this->getSmsAdapter($provider), MESSAGE_TYPE_PUSH => $this->getPushAdapter($provider), MESSAGE_TYPE_EMAIL => $this->getEmailAdapter($provider), - default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) + default => throw new \Exception('Provider with the requested ID is of the incorrect type') }; - $maxBatchSize = $adapter->getMaxMessagesPerRequest(); - $batches = \array_chunk($identifiers, $maxBatchSize); - $batchIndex = 0; + $batches = \array_chunk( + \array_keys($identifiersForProvider), + $adapter->getMaxMessagesPerRequest() + ); - return batch(\array_map(function ($batch) use ($message, $provider, $adapter, &$batchIndex, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { - return function () use ($batch, $message, $provider, $adapter, &$batchIndex, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { + return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { + return function () use ($batch, $message, $provider, $adapter, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { $deliveredTotal = 0; $deliveryErrors = []; $messageData = clone $message; @@ -256,7 +258,7 @@ class Messaging extends Action MESSAGE_TYPE_SMS => $this->buildSmsMessage($messageData, $provider), MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData), MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider, $deviceForFiles, $deviceForLocalFiles), - default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) + default => throw new \Exception('Provider with the requested ID is of the incorrect type') }; try { @@ -283,10 +285,8 @@ class Messaging extends Action } } } catch (\Throwable $e) { - $deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . ' of ' . \count($batch) . ' with error: ' . $e->getMessage(); + $deliveryErrors[] = 'Failed sending to targets with error: ' . $e->getMessage(); } finally { - $batchIndex++; - return [ 'deliveredTotal' => $deliveredTotal, 'deliveryErrors' => $deliveryErrors, @@ -297,7 +297,7 @@ class Messaging extends Action }; }, \array_keys($identifiers))); - $results = array_merge(...$results); + $results = \array_merge(...$results); $deliveredTotal = 0; $deliveryErrors = []; @@ -330,7 +330,7 @@ class Messaging extends Action $dbForProject->updateDocument('messages', $message->getId(), $message); - // Delete any attachments that were downloaded to the local cache + // Delete any attachments that were downloaded to local storage if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) { if ($deviceForFiles->getType() === Storage::DEVICE_LOCAL) { return; @@ -345,12 +345,12 @@ class Messaging extends Action $bucket = $dbForProject->getDocument('buckets', $bucketId); if ($bucket->isEmpty()) { - throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + throw new \Exception('Storage bucket with the requested ID could not be found'); } $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); if ($file->isEmpty()) { - throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + throw new \Exception('Storage file with the requested ID could not be found'); } $path = $file->getAttribute('path', ''); @@ -369,7 +369,7 @@ class Messaging extends Action } if ($project->isEmpty()) { - throw new Exception('Project not set in payload'); + throw new \Exception('Project not set in payload'); } Console::log('Project: ' . $project->getId()); @@ -427,12 +427,13 @@ class Messaging extends Action $adapter = $this->getSmsAdapter($provider); - $maxBatchSize = $adapter->getMaxMessagesPerRequest(); - $batches = \array_chunk($recipients, $maxBatchSize); - $batchIndex = 0; + $batches = \array_chunk( + $recipients, + $adapter->getMaxMessagesPerRequest() + ); - batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $project, $queueForUsage) { - return function () use ($batch, $message, $provider, $adapter, $batchIndex, $project, $queueForUsage) { + batch(\array_map(function ($batch) use ($message, $provider, $adapter, $project, $queueForUsage) { + return function () use ($batch, $message, $provider, $adapter, $project, $queueForUsage) { $message->setAttribute('to', $batch); $data = $this->buildSmsMessage($message, $provider); @@ -445,7 +446,7 @@ class Messaging extends Action ->addMetric(METRIC_MESSAGES, 1) ->trigger(); } catch (\Throwable $e) { - throw new Exception('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage(), 500); + throw new \Exception('Failed sending to targets with error: ' . $e->getMessage()); } }; }, $batches)); @@ -556,19 +557,19 @@ class Messaging extends Action $bucket = $dbForProject->getDocument('buckets', $bucketId); if ($bucket->isEmpty()) { - throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + throw new \Exception('Storage bucket with the requested ID could not be found'); } $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); if ($file->isEmpty()) { - throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + throw new \Exception('Storage file with the requested ID could not be found'); } $mimes = Config::getParam('storage-mimes'); $path = $file->getAttribute('path', ''); if (!$deviceForFiles->exists($path)) { - throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path); + throw new \Exception('File not found in ' . $path); } $contentType = 'text/plain'; diff --git a/src/Appwrite/Utopia/Response/Filters/V17.php b/src/Appwrite/Utopia/Response/Filters/V17.php index cf62bcf488..b2d47e9e13 100644 --- a/src/Appwrite/Utopia/Response/Filters/V17.php +++ b/src/Appwrite/Utopia/Response/Filters/V17.php @@ -22,6 +22,15 @@ class V17 extends Filter case Response::MODEL_TOKEN: $parsedResponse = $this->parseToken($parsedResponse); break; + case Response::MODEL_MEMBERSHIP: + $parsedResponse = $this->parseMembership($parsedResponse); + break; + case Response::MODEL_SESSION: + $parsedResponse = $this->parseSession($parsedResponse); + break; + case Response::MODEL_WEBHOOK: + $parsedResponse = $this->parseWebhook($parsedResponse); + break; } return $parsedResponse; @@ -30,6 +39,8 @@ class V17 extends Filter protected function parseUser(array $content) { unset($content['targets']); + unset($content['mfa']); + unset($content['totp']); return $content; } @@ -45,4 +56,25 @@ class V17 extends Filter unset($content['phrase']); return $content; } + + protected function parseMembership(array $content) + { + unset($content['mfa']); + return $content; + } + + protected function parseSession(array $content) + { + unset($content['factors']); + unset($content['secret']); + return $content; + } + + protected function parseWebhook(array $content) + { + unset($content['enabled']); + unset($content['logs']); + unset($content['attempts']); + return $content; + } } diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 84b740a988..9b0d2ee0e9 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -74,7 +74,9 @@ class AccountCustomClientTest extends Scope $this->assertEmpty($response['body']['secret']); $this->assertNotFalse(\DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $response['body']['expire'])); - // already logged in + /** + * Test for FAILURE + */ $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', @@ -85,11 +87,8 @@ class AccountCustomClientTest extends Scope 'password' => $password, ]); - $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(401, $response['headers']['status-code']); - /** - * Test for FAILURE - */ $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', @@ -233,10 +232,7 @@ class AccountCustomClientTest extends Scope ])); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertIsArray($response['body']); - $this->assertNotEmpty($response['body']); - $this->assertCount(2, $response['body']); - $this->assertEquals(3, $response['body']['total']); + $this->assertEquals(2, $response['body']['total']); $this->assertEquals($sessionId, $response['body']['sessions'][0]['$id']); $this->assertEquals('Windows', $response['body']['sessions'][0]['osName']); @@ -293,9 +289,9 @@ class AccountCustomClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertIsArray($response['body']['logs']); $this->assertNotEmpty($response['body']['logs']); - $this->assertCount(4, $response['body']['logs']); + $this->assertCount(3, $response['body']['logs']); $this->assertIsNumeric($response['body']['total']); - $this->assertEquals("session.create", $response['body']['logs'][2]['event']); + $this->assertEquals("user.create", $response['body']['logs'][2]['event']); $this->assertEquals(filter_var($response['body']['logs'][2]['ip'], FILTER_VALIDATE_IP), $response['body']['logs'][2]['ip']); $this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['logs'][2]['time'])); @@ -317,10 +313,6 @@ class AccountCustomClientTest extends Scope $this->assertEquals('--', $response['body']['logs'][1]['countryCode']); $this->assertEquals('Unknown', $response['body']['logs'][1]['countryName']); - $this->assertEquals("user.create", $response['body']['logs'][3]['event']); - $this->assertEquals(filter_var($response['body']['logs'][3]['ip'], FILTER_VALIDATE_IP), $response['body']['logs'][3]['ip']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['logs'][2]['time'])); - $this->assertEquals('Windows', $response['body']['logs'][2]['osName']); $this->assertEquals('WIN', $response['body']['logs'][2]['osCode']); $this->assertEquals('10', $response['body']['logs'][2]['osVersion']); @@ -372,7 +364,7 @@ class AccountCustomClientTest extends Scope $this->assertEquals($responseOffset['headers']['status-code'], 200); $this->assertIsArray($responseOffset['body']['logs']); $this->assertNotEmpty($responseOffset['body']['logs']); - $this->assertCount(3, $responseOffset['body']['logs']); + $this->assertCount(2, $responseOffset['body']['logs']); $this->assertIsNumeric($responseOffset['body']['total']); $this->assertEquals($response['body']['logs'][1], $responseOffset['body']['logs'][0]); @@ -2239,7 +2231,6 @@ class AccountCustomClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ])); $this->assertEquals(201, $response['headers']['status-code']); @@ -2249,6 +2240,9 @@ class AccountCustomClientTest extends Scope $smsRequest = $this->getLastRequest(); + $message = $smsRequest['data']['message']; + $token = substr($message, 0, 6); + return \array_merge($data, [ 'token' => \substr($smsRequest['data']['message'], 0, 6) ]); diff --git a/tests/e2e/Services/Account/AccountCustomServerTest.php b/tests/e2e/Services/Account/AccountCustomServerTest.php index e65e574535..e29110cf42 100644 --- a/tests/e2e/Services/Account/AccountCustomServerTest.php +++ b/tests/e2e/Services/Account/AccountCustomServerTest.php @@ -62,7 +62,9 @@ class AccountCustomServerTest extends Scope $this->assertNotEmpty($response['body']['secret']); $this->assertNotFalse(\DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $response['body']['expire'])); - // already logged in + /** + * Test for FAILURE + */ $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -72,11 +74,8 @@ class AccountCustomServerTest extends Scope 'password' => $password, ]); - $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(401, $response['headers']['status-code']); - /** - * Test for FAILURE - */ $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], diff --git a/tests/e2e/Services/GraphQL/BatchTest.php b/tests/e2e/Services/GraphQL/BatchTest.php index 55bce6eae3..7d154de802 100644 --- a/tests/e2e/Services/GraphQL/BatchTest.php +++ b/tests/e2e/Services/GraphQL/BatchTest.php @@ -281,19 +281,22 @@ class BatchTest extends Scope public function testQueryBatchedMutations() { $projectId = $this->getProject()['$id']; - $email = 'tester' . \uniqid() . '@example.com'; + $email1 = 'tester' . \uniqid() . '@example.com'; + $email2 = 'tester' . \uniqid() . '@example.com'; $graphQLPayload = [ - 'query' => 'mutation CreateAndLogin($userId: String!, $email: String!, $password: String!, $name: String) { - accountCreate(userId: $userId, email: $email, password: $password, name: $name) { - name + 'query' => 'mutation CreateAndLogin($user1Id: String!, $user2Id: String!, $email1: String!, $email2: String!, $password: String!, $name: String) { + account1: accountCreate(userId: $user1Id, email: $email1, password: $password, name: $name) { + email } - accountCreateEmailPasswordSession(email: $email, password: $password) { - expire + account2: accountCreate(userId: $user2Id, email: $email2, password: $password, name: $name) { + email } }', 'variables' => [ - 'userId' => ID::unique(), - 'email' => $email, + 'user1Id' => ID::unique(), + 'user2Id' => ID::unique(), + 'email1' => $email1, + 'email2' => $email2, 'password' => 'password', 'name' => 'Tester', ], @@ -304,12 +307,12 @@ class BatchTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $graphQLPayload); - $this->assertIsArray($response['body']['data']); $this->assertArrayNotHasKey('errors', $response['body']); - $this->assertArrayHasKey('accountCreate', $response['body']['data']); - $this->assertArrayHasKey('accountCreateEmailPasswordSession', $response['body']['data']); - $this->assertEquals('Tester', $response['body']['data']['accountCreate']['name']); + $this->assertArrayHasKey('account1', $response['body']['data']); + $this->assertArrayHasKey('account2', $response['body']['data']); + $this->assertEquals($email1, $response['body']['data']['account1']['email']); + $this->assertEquals($email2, $response['body']['data']['account2']['email']); } public function testQueryBatchedMutationsOfSameType() diff --git a/tests/unit/Utopia/Response/Filters/V17Test.php b/tests/unit/Utopia/Response/Filters/V17Test.php index 25f4fb2f2e..136211a422 100644 --- a/tests/unit/Utopia/Response/Filters/V17Test.php +++ b/tests/unit/Utopia/Response/Filters/V17Test.php @@ -73,6 +73,8 @@ class V17Test extends TestCase 'remove targets' => [ [ 'targets' => 'test', + 'mfa' => 'test', + 'totp' => 'test', ], [ ], @@ -116,4 +118,70 @@ class V17Test extends TestCase $this->assertEquals($expected, $result); } + + public function membershipProvider(): array + { + return [ + 'remove mfa' => [ + [ + 'mfa' => 'test', + ], + [ + ], + ], + ]; + } + + /** + * @dataProvider membershipProvider + */ + public function testMembership(array $content, array $expected): void + { + $model = Response::MODEL_MEMBERSHIP; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public function sessionProvider(): array + { + return [ + 'remove factors and secrets' => [ + [ + 'factors' => 'test', + 'secret' => 'test', + ], + [ + ], + ] + ]; + } + + /** + * @dataProvider sessionProvider + */ + public function testSession(array $content, array $expected): void + { + $model = Response::MODEL_SESSION; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public function webhookProvider(): array + { + return [ + 'remove webhook additions' => [ + [ + 'enabled' => true, + 'logs' => ['test', 'test'], + 'attempts' => 1 + ], + [ + ], + ], + ]; + } }