diff --git a/.env b/.env
index 256f2c984a..4fbe3333b4 100644
--- a/.env
+++ b/.env
@@ -22,7 +22,7 @@ _APP_OPTIONS_FORCE_HTTPS=disabled
_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled
_APP_OPENSSL_KEY_V1=your-secret-key
_APP_DNS=8.8.8.8
-_APP_DOMAIN=traefik
+_APP_DOMAIN=appwrite.test
_APP_CONSOLE_DOMAIN=localhost
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_SITES=sites.localhost
@@ -103,6 +103,7 @@ _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_USAGE_STATS=enabled
_APP_LOGGING_CONFIG=
+_APP_LOGGING_CONFIG_REALTIME=
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
_APP_GRAPHQL_MAX_DEPTH=4
@@ -123,4 +124,4 @@ _APP_MESSAGE_PUSH_TEST_DSN=
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
_APP_PROJECT_REGIONS=default
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
-_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
\ No newline at end of file
+_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
diff --git a/Dockerfile b/Dockerfile
index 2a3e176838..e932297671 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
-FROM appwrite/base:0.10.4 AS final
+FROM appwrite/base:0.10.6 AS final
LABEL maintainer="team@appwrite.io"
@@ -24,11 +24,9 @@ ENV _APP_VERSION=$VERSION \
_APP_HOME=https://appwrite.io
RUN \
- if [ "$DEBUG" == "true" ]; then \
+ if [ "$DEBUG" == "true" ]; then \
apk add boost boost-dev; \
- fi
-
-RUN apk add libwebp
+ fi
WORKDIR /usr/src/code
@@ -98,6 +96,7 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
# Enable Extensions
RUN if [ "$DEBUG" = "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
RUN if [ "$DEBUG" = "true" ]; then mkdir -p /tmp/xdebug; fi
+RUN if [ "$DEBUG" = "true" ]; then apk add --update --no-cache openssh-client github-cli; fi
RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xdebug.so; fi
diff --git a/app/assets/dbip/dbip-country-lite-2024-09.mmdb b/app/assets/dbip/dbip-country-lite-2024-09.mmdb
deleted file mode 100644
index 43d6bcdeea..0000000000
Binary files a/app/assets/dbip/dbip-country-lite-2024-09.mmdb and /dev/null differ
diff --git a/app/assets/dbip/dbip-country-lite-2025-12.mmdb b/app/assets/dbip/dbip-country-lite-2025-12.mmdb
new file mode 100644
index 0000000000..4ecabbf735
Binary files /dev/null and b/app/assets/dbip/dbip-country-lite-2025-12.mmdb differ
diff --git a/app/cli.php b/app/cli.php
index 71b6464cb9..73134887ea 100644
--- a/app/cli.php
+++ b/app/cli.php
@@ -9,6 +9,7 @@ use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
+use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Swoole\Timer;
@@ -76,6 +77,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
+ $dbForPlatform->setDocumentType('users', User::class);
// Ensure tables exist
$collections = Config::getParam('collections', [])['console'];
diff --git a/app/config/collections/common.php b/app/config/collections/common.php
index 6de7eb224b..a364a0a866 100644
--- a/app/config/collections/common.php
+++ b/app/config/collections/common.php
@@ -1,6 +1,6 @@
256,
'signed' => true,
'required' => false,
- 'default' => Auth::DEFAULT_ALGO,
+ 'default' => (new Argon2())->getName(),
'array' => false,
'filters' => [],
],
@@ -184,7 +184,7 @@ return [
'size' => 65535,
'signed' => true,
'required' => false,
- 'default' => Auth::DEFAULT_ALGO_OPTIONS,
+ 'default' => (new Argon2())->getOptions(),
'array' => false,
'filters' => ['json'],
],
@@ -364,6 +364,61 @@ return [
'array' => false,
'filters' => ['datetime'],
],
+ [
+ '$id' => ID::custom('emailCanonical'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 320,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('emailIsFree'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('emailIsDisposable'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('emailIsCorporate'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('emailIsCanonical'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
],
'indexes' => [
[
@@ -1527,6 +1582,17 @@ return [
'required' => true,
'array' => false,
],
+ [
+ '$id' => ID::custom('transformations'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'signed' => true,
+ 'size' => 0,
+ 'format' => '',
+ 'filters' => [],
+ 'required' => false,
+ 'array' => false,
+ 'default' => true,
+ ],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php
index bf0cee3527..dae0337dc9 100644
--- a/app/config/collections/projects.php
+++ b/app/config/collections/projects.php
@@ -2345,7 +2345,7 @@ return [
'$id' => ID::custom('errors'),
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 65535,
+ 'size' => 1_000_000,
'signed' => true,
'required' => true,
'default' => null,
diff --git a/app/config/console.php b/app/config/console.php
index f8f68a8039..5c4bf87614 100644
--- a/app/config/console.php
+++ b/app/config/console.php
@@ -4,7 +4,6 @@
* Initializes console project document.
*/
-use Appwrite\Auth\Auth;
use Appwrite\Network\Platform;
use Utopia\Database\Helpers\ID;
use Utopia\System\System;
@@ -38,7 +37,7 @@ $console = [
'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
+ 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
'invalidateSessions' => true
],
diff --git a/app/config/errors.php b/app/config/errors.php
index 2e18f05797..6d747e4eb1 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -522,6 +522,11 @@ return [
'description' => 'The requested file is not publicly readable.',
'code' => 403,
],
+ Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED => [
+ 'name' => Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED,
+ 'description' => 'Image transformations are disabled for the requested bucket.',
+ 'code' => 403,
+ ],
/** Tokens */
Exception::TOKEN_NOT_FOUND => [
@@ -683,12 +688,12 @@ return [
/** Databases */
Exception::DATABASE_NOT_FOUND => [
'name' => Exception::DATABASE_NOT_FOUND,
- 'description' => 'Database not found',
+ 'description' => 'Database with the requested ID \'%s\' could not be found.',
'code' => 404
],
Exception::DATABASE_ALREADY_EXISTS => [
'name' => Exception::DATABASE_ALREADY_EXISTS,
- 'description' => 'Database already exists',
+ 'description' => 'Database with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409
],
Exception::DATABASE_TIMEOUT => [
@@ -705,41 +710,41 @@ return [
/** Collections */
Exception::COLLECTION_NOT_FOUND => [
'name' => Exception::COLLECTION_NOT_FOUND,
- 'description' => 'Collection with the requested ID could not be found.',
+ 'description' => 'Collection with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::COLLECTION_ALREADY_EXISTS => [
'name' => Exception::COLLECTION_ALREADY_EXISTS,
- 'description' => 'A collection with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'description' => 'A collection with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::COLLECTION_LIMIT_EXCEEDED => [
'name' => Exception::COLLECTION_LIMIT_EXCEEDED,
- 'description' => 'The maximum number of collections has been reached.',
+ 'description' => 'The maximum number of collections for database \'%s\' has been reached.',
'code' => 400,
],
/** Tables */
Exception::TABLE_NOT_FOUND => [
'name' => Exception::TABLE_NOT_FOUND,
- 'description' => 'Table with the requested ID could not be found.',
+ 'description' => 'Table with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::TABLE_ALREADY_EXISTS => [
'name' => Exception::TABLE_ALREADY_EXISTS,
- 'description' => 'A table with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'description' => 'A table with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::TABLE_LIMIT_EXCEEDED => [
'name' => Exception::TABLE_LIMIT_EXCEEDED,
- 'description' => 'The maximum number of tables has been reached.',
+ 'description' => 'The maximum number of tables for database \'%s\' has been reached.',
'code' => 400,
],
/** Documents */
Exception::DOCUMENT_NOT_FOUND => [
'name' => Exception::DOCUMENT_NOT_FOUND,
- 'description' => 'Document with the requested ID could not be found.',
+ 'description' => 'Document with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::DOCUMENT_INVALID_STRUCTURE => [
@@ -759,7 +764,7 @@ return [
],
Exception::DOCUMENT_ALREADY_EXISTS => [
'name' => Exception::DOCUMENT_ALREADY_EXISTS,
- 'description' => 'Document with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'description' => 'Document with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::DOCUMENT_UPDATE_CONFLICT => [
@@ -776,7 +781,7 @@ return [
/** Rows */
Exception::ROW_NOT_FOUND => [
'name' => Exception::ROW_NOT_FOUND,
- 'description' => 'Row with the requested ID could not be found.',
+ 'description' => 'Row with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::ROW_INVALID_STRUCTURE => [
@@ -786,7 +791,7 @@ return [
],
Exception::ROW_MISSING_DATA => [
'name' => Exception::ROW_MISSING_DATA,
- 'description' => 'The row data is missing. Try again with row data populated',
+ 'description' => 'The row data is missing. Try again with row data populated.',
'code' => 400,
],
Exception::ROW_MISSING_PAYLOAD => [
@@ -796,7 +801,7 @@ return [
],
Exception::ROW_ALREADY_EXISTS => [
'name' => Exception::ROW_ALREADY_EXISTS,
- 'description' => 'Row with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'description' => 'Row with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::ROW_UPDATE_CONFLICT => [
@@ -813,17 +818,17 @@ return [
/** Attributes */
Exception::ATTRIBUTE_NOT_FOUND => [
'name' => Exception::ATTRIBUTE_NOT_FOUND,
- 'description' => 'Attribute with the requested ID could not be found.',
+ 'description' => 'Attribute with the requested key \'%s\' could not be found.',
'code' => 404,
],
Exception::ATTRIBUTE_UNKNOWN => [
'name' => Exception::ATTRIBUTE_UNKNOWN,
- 'description' => 'The attribute required for the index could not be found. Please confirm all your attributes are in the available state.',
+ 'description' => 'The attribute \'%s\' required for the index could not be found. Please confirm all your attributes are in the available state.',
'code' => 400,
],
Exception::ATTRIBUTE_NOT_AVAILABLE => [
'name' => Exception::ATTRIBUTE_NOT_AVAILABLE,
- 'description' => 'The requested attribute is not yet available. Please try again later.',
+ 'description' => 'The requested attribute \'%s\' is not yet available. Please try again later.',
'code' => 400,
],
Exception::ATTRIBUTE_FORMAT_UNSUPPORTED => [
@@ -838,12 +843,12 @@ return [
],
Exception::ATTRIBUTE_ALREADY_EXISTS => [
'name' => Exception::ATTRIBUTE_ALREADY_EXISTS,
- 'description' => 'Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.',
+ 'description' => 'Attribute with the requested key \'%s\' already exists. Attribute keys must be unique, try again with a different key.',
'code' => 409,
],
Exception::ATTRIBUTE_LIMIT_EXCEEDED => [
'name' => Exception::ATTRIBUTE_LIMIT_EXCEEDED,
- 'description' => 'The maximum number or size of attributes for this collection has been reached.',
+ 'description' => 'The maximum number or size of attributes for collection \'%s\' has been reached.',
'code' => 400,
],
Exception::ATTRIBUTE_VALUE_INVALID => [
@@ -853,7 +858,7 @@ return [
],
Exception::ATTRIBUTE_TYPE_INVALID => [
'name' => Exception::ATTRIBUTE_TYPE_INVALID,
- 'description' => 'The attribute type is invalid.',
+ 'description' => 'The attribute \'%s\' type is invalid.',
'code' => 400,
],
Exception::ATTRIBUTE_INVALID_RESIZE => [
@@ -864,7 +869,7 @@ return [
Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED => [
'name' => Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED,
- 'description' => 'Attribute type is not supported.',
+ 'description' => 'Attribute type \'%s\' is not supported.',
'code' => 400,
],
@@ -878,17 +883,17 @@ return [
/** Columns */
Exception::COLUMN_NOT_FOUND => [
'name' => Exception::COLUMN_NOT_FOUND,
- 'description' => 'Column with the requested ID could not be found.',
+ 'description' => 'Column with the requested key \'%s\' could not be found.',
'code' => 404,
],
Exception::COLUMN_UNKNOWN => [
'name' => Exception::COLUMN_UNKNOWN,
- 'description' => 'The column required for the index could not be found. Please confirm all your columns are in the available state.',
+ 'description' => 'The column \'%s\' required for the index could not be found. Please confirm all your columns are in the available state.',
'code' => 400,
],
Exception::COLUMN_NOT_AVAILABLE => [
'name' => Exception::COLUMN_NOT_AVAILABLE,
- 'description' => 'The requested column is not yet available. Please try again later.',
+ 'description' => 'The requested column \'%s\' is not yet available. Please try again later.',
'code' => 400,
],
Exception::COLUMN_FORMAT_UNSUPPORTED => [
@@ -903,12 +908,12 @@ return [
],
Exception::COLUMN_ALREADY_EXISTS => [
'name' => Exception::COLUMN_ALREADY_EXISTS,
- 'description' => 'Column with the requested key already exists. Column keys must be unique, try again with a different key.',
+ 'description' => 'Column with the requested key \'%s\' already exists. Column keys must be unique, try again with a different key.',
'code' => 409,
],
Exception::COLUMN_LIMIT_EXCEEDED => [
'name' => Exception::COLUMN_LIMIT_EXCEEDED,
- 'description' => 'The maximum number or size of columns for this table has been reached.',
+ 'description' => 'The maximum number or size of columns for table \'%s\' has been reached.',
'code' => 400,
],
Exception::COLUMN_VALUE_INVALID => [
@@ -918,7 +923,7 @@ return [
],
Exception::COLUMN_TYPE_INVALID => [
'name' => Exception::COLUMN_TYPE_INVALID,
- 'description' => 'The column type is invalid.',
+ 'description' => 'The column \'%s\' type is invalid.',
'code' => 400,
],
Exception::COLUMN_INVALID_RESIZE => [
@@ -928,24 +933,24 @@ return [
],
Exception::COLUMN_TYPE_NOT_SUPPORTED => [
'name' => Exception::COLUMN_TYPE_NOT_SUPPORTED,
- 'description' => 'Column type is not supported.',
+ 'description' => 'Column type \'%s\' is not supported.',
'code' => 400,
],
/** Indexes */
Exception::INDEX_NOT_FOUND => [
'name' => Exception::INDEX_NOT_FOUND,
- 'description' => 'Index with the requested ID could not be found.',
+ 'description' => 'Index with the requested key \'%s\' could not be found.',
'code' => 404,
],
Exception::INDEX_LIMIT_EXCEEDED => [
'name' => Exception::INDEX_LIMIT_EXCEEDED,
- 'description' => 'The maximum number of indexes has been reached.',
+ 'description' => 'The maximum number of indexes for collection \'%s\' has been reached.',
'code' => 400,
],
Exception::INDEX_ALREADY_EXISTS => [
'name' => Exception::INDEX_ALREADY_EXISTS,
- 'description' => 'Index with the requested key already exists. Try again with a different key.',
+ 'description' => 'Index with the requested key \'%s\' already exists. Try again with a different key.',
'code' => 409,
],
Exception::INDEX_INVALID => [
@@ -955,24 +960,24 @@ return [
],
Exception::INDEX_DEPENDENCY => [
'name' => Exception::INDEX_DEPENDENCY,
- 'description' => 'Attribute cannot be renamed or deleted. Please remove the associated index first.',
+ 'description' => 'Attribute \'%s\' cannot be renamed or deleted. Please remove the associated index first.',
'code' => 409,
],
/** Column Indexes, same as Indexes but with different type */
Exception::COLUMN_INDEX_NOT_FOUND => [
'name' => Exception::COLUMN_INDEX_NOT_FOUND,
- 'description' => 'Index with the requested ID could not be found.',
+ 'description' => 'Index with the requested key \'%s\' could not be found.',
'code' => 404,
],
Exception::COLUMN_INDEX_LIMIT_EXCEEDED => [
'name' => Exception::COLUMN_INDEX_LIMIT_EXCEEDED,
- 'description' => 'The maximum number of indexes has been reached.',
+ 'description' => 'The maximum number of indexes for table \'%s\' has been reached.',
'code' => 400,
],
Exception::COLUMN_INDEX_ALREADY_EXISTS => [
'name' => Exception::COLUMN_INDEX_ALREADY_EXISTS,
- 'description' => 'Index with the requested key already exists. Try again with a different key.',
+ 'description' => 'Index with the requested key \'%s\' already exists. Try again with a different key.',
'code' => 409,
],
Exception::COLUMN_INDEX_INVALID => [
@@ -982,19 +987,19 @@ return [
],
Exception::COLUMN_INDEX_DEPENDENCY => [
'name' => Exception::COLUMN_INDEX_DEPENDENCY,
- 'description' => 'Column cannot be renamed or deleted. Please remove the associated index first.',
+ 'description' => 'Column \'%s\' cannot be renamed or deleted. Please remove the associated index first.',
'code' => 409,
],
/** Transactions */
Exception::TRANSACTION_NOT_FOUND => [
'name' => Exception::TRANSACTION_NOT_FOUND,
- 'description' => 'Transaction with the requested ID could not be found.',
+ 'description' => 'Transaction with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::TRANSACTION_ALREADY_EXISTS => [
'name' => Exception::TRANSACTION_ALREADY_EXISTS,
- 'description' => 'Transaction with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'description' => 'Transaction with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::TRANSACTION_INVALID => [
diff --git a/app/config/frameworks.php b/app/config/frameworks.php
index f4d8ec7ffa..6078c53c63 100644
--- a/app/config/frameworks.php
+++ b/app/config/frameworks.php
@@ -8,20 +8,13 @@ use Utopia\Config\Config;
$templateRuntimes = Config::getParam('template-runtimes');
-function getVersions(array $versions, string $prefix)
-{
- return array_map(function ($version) use ($prefix) {
- return $prefix . '-' . $version;
- }, $versions);
-}
-
return [
'analog' => [
'key' => 'analog',
'name' => 'Analog',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/analog/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/analog/env.sh',
'adapters' => [
@@ -47,7 +40,7 @@ return [
'name' => 'Angular',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/angular/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/angular/env.sh',
'adapters' => [
@@ -73,7 +66,7 @@ return [
'name' => 'Next.js',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/next-js/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/next-js/env.sh',
'adapters' => [
@@ -98,7 +91,7 @@ return [
'name' => 'React',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -115,7 +108,7 @@ return [
'name' => 'Nuxt',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/nuxt/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/nuxt/env.sh',
'adapters' => [
@@ -140,7 +133,7 @@ return [
'name' => 'Vue.js',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -157,7 +150,7 @@ return [
'name' => 'SvelteKit',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/sveltekit/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/sveltekit/env.sh',
'adapters' => [
@@ -182,7 +175,7 @@ return [
'name' => 'Astro',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/astro/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/astro/env.sh',
'adapters' => [
@@ -202,12 +195,37 @@ return [
]
]
],
+ 'tanstack-start' => [
+ 'key' => 'tanstack-start',
+ 'name' => 'TanStack Start',
+ 'screenshotSleep' => 3000,
+ 'buildRuntime' => 'node-22',
+ 'runtimes' => $templateRuntimes['NODE'],
+ 'bundleCommand' => 'bash /usr/local/server/helpers/tanstack-start/bundle.sh',
+ 'envCommand' => 'source /usr/local/server/helpers/tanstack-start/env.sh',
+ 'adapters' => [
+ 'ssr' => [
+ 'key' => 'ssr',
+ 'buildCommand' => 'npm run build',
+ 'installCommand' => 'npm install',
+ 'outputDirectory' => './.output',
+ 'startCommand' => 'bash helpers/tanstack-start/server.sh',
+ ],
+ 'static' => [
+ 'key' => 'static',
+ 'buildCommand' => 'npm run build',
+ 'installCommand' => 'npm install',
+ 'outputDirectory' => './dist/client',
+ 'startCommand' => 'bash helpers/server.sh',
+ ]
+ ]
+ ],
'remix' => [
'key' => 'remix',
'name' => 'Remix',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/remix/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/remix/env.sh',
'adapters' => [
@@ -232,7 +250,7 @@ return [
'name' => 'Lynx',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -248,8 +266,8 @@ return [
'key' => 'flutter',
'name' => 'Flutter',
'screenshotSleep' => 5000,
- 'buildRuntime' => 'flutter-3.29',
- 'runtimes' => getVersions($templateRuntimes['FLUTTER']['versions'], 'flutter'),
+ 'buildRuntime' => 'flutter-3.35',
+ 'runtimes' => $templateRuntimes['FLUTTER'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -257,6 +275,7 @@ return [
'installCommand' => 'flutter pub get',
'outputDirectory' => './build/web',
'startCommand' => 'bash helpers/server.sh',
+ 'fallbackFile' => 'index.html'
],
],
],
@@ -265,7 +284,7 @@ return [
'name' => 'React Native',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -282,7 +301,7 @@ return [
'name' => 'Vite',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -298,7 +317,7 @@ return [
'name' => 'Other',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
- 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
+ 'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
diff --git a/app/config/locale/templates/email-base-styled.tpl b/app/config/locale/templates/email-base-styled.tpl
index 37ca630d43..1979d560b5 100644
--- a/app/config/locale/templates/email-base-styled.tpl
+++ b/app/config/locale/templates/email-base-styled.tpl
@@ -2,6 +2,38 @@
+
+
+
-
+
+
+
Themed website
+
Adaptive light and dark mode showcase
+
Appwrite Sites
+
+
diff --git a/tests/resources/sites/static-themed/screenshot-dark.png b/tests/resources/sites/static-themed/screenshot-dark.png
new file mode 100644
index 0000000000..199a09b902
Binary files /dev/null and b/tests/resources/sites/static-themed/screenshot-dark.png differ
diff --git a/tests/resources/sites/static-themed/screenshot-light.png b/tests/resources/sites/static-themed/screenshot-light.png
new file mode 100644
index 0000000000..4eae73c5b7
Binary files /dev/null and b/tests/resources/sites/static-themed/screenshot-light.png differ
diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php
deleted file mode 100644
index 705da42879..0000000000
--- a/tests/unit/Auth/AuthTest.php
+++ /dev/null
@@ -1,502 +0,0 @@
-toString());
- }
-
- public function testCookieName(): void
- {
- $name = 'cookie-name';
-
- $this->assertEquals(Auth::setCookieName($name), $name);
- $this->assertEquals(Auth::$cookieName, $name);
- }
-
- public function testEncodeDecodeSession(): void
- {
- $id = 'id';
- $secret = 'secret';
- $session = 'eyJpZCI6ImlkIiwic2VjcmV0Ijoic2VjcmV0In0=';
-
- $this->assertEquals(Auth::encodeSession($id, $secret), $session);
- $this->assertEquals(Auth::decodeSession($session), ['id' => $id, 'secret' => $secret]);
- }
-
- public function testHash(): void
- {
- $secret = 'secret';
- $this->assertEquals(Auth::hash($secret), '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b');
- }
-
- public function testPassword(): void
- {
- /*
- General tests, using pre-defined hashes generated by online tools
- */
-
- // Bcrypt - Version Y
- $plain = 'secret';
- $hash = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy';
- $generatedHash = Auth::passwordHash($plain, 'bcrypt');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
-
- // Bcrypt - Version A
- $plain = 'test123';
- $hash = '$2a$12$3f2ZaARQ1AmhtQWx2nmQpuXcWfTj1YV2/Hl54e8uKxIzJe3IfwLiu';
- $generatedHash = Auth::passwordHash($plain, 'bcrypt');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
-
- // Bcrypt - Cost 5
- $plain = 'hello-world';
- $hash = '$2a$05$IjrtSz6SN7UJ6Sh3l.b5jODEvEG2LMJTPAHIaLWRvlWx7if3VMkFO';
- $generatedHash = Auth::passwordHash($plain, 'bcrypt');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
-
- // Bcrypt - Cost 15
- $plain = 'super-secret-password';
- $hash = '$2a$15$DS0ZzbsFZYumH/E4Qj5oeOHnBcM3nCCsCA2m4Goigat/0iMVQC4Na';
- $generatedHash = Auth::passwordHash($plain, 'bcrypt');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
-
- // MD5 - Short
- $plain = 'appwrite';
- $hash = '144fa7eaa4904e8ee120651997f70dcc';
- $generatedHash = Auth::passwordHash($plain, 'md5');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
-
- // MD5 - Long
- $plain = 'AppwriteIsAwesomeBackendAsAServiceThatIsAlsoOpenSourced';
- $hash = '8410e96cf7ac64e0b84c3f8517a82616';
- $generatedHash = Auth::passwordHash($plain, 'md5');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
-
- // PHPass
- $plain = 'pass123';
- $hash = '$P$BVKPmJBZuLch27D4oiMRTEykGLQ9tX0';
- $generatedHash = Auth::passwordHash($plain, 'phpass');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
-
- // SHA
- $plain = 'developersAreAwesome!';
- $hash = '2455118438cb125354b89bb5888346e9bd23355462c40df393fab514bf2220b5a08e4e2d7b85d7327595a450d0ac965cc6661152a46a157c66d681bed20a4735';
- $generatedHash = Auth::passwordHash($plain, 'sha');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'sha'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'sha'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'sha'));
-
- // Argon2
- $plain = 'safe-argon-password';
- $hash = '$argon2id$v=19$m=2048,t=3,p=4$MWc5NWRmc2QxZzU2$41mp7rSgBZ49YxLbbxIac7aRaxfp5/e1G45ckwnK0g8';
- $generatedHash = Auth::passwordHash($plain, 'argon2');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'argon2'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'argon2'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'argon2'));
-
- // Scrypt
- $plain = 'some-scrypt-password';
- $hash = 'b448ad7ba88b653b5b56b8053a06806724932d0751988bc9cd0ef7ff059e8ba8a020e1913b7069a650d3f99a1559aba0221f2c277826919513a054e76e339028';
- $generatedHash = Auth::passwordHash($plain, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]);
-
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
- $this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-wrong-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
- $this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 10, 'costParallel' => 2]));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
-
- // ScryptModified tested are in provider-specific tests below
-
- /*
- Provider-specific tests, ensuring functionality of specific use-cases
- */
-
- // Provider #1 (Database)
- $plain = 'example-password';
- $hash = '$2a$10$3bIGRWUes86CICsuchGLj.e.BqdCdg2/1Ud9LvBhJr0j7Dze8PBdS';
- $generatedHash = Auth::passwordHash($plain, 'bcrypt');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
-
- // Provider #2 (Blog)
- $plain = 'your-password';
- $hash = '$P$BkiNDJTpAWXtpaMhEUhUdrv7M0I1g6.';
- $generatedHash = Auth::passwordHash($plain, 'phpass');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
-
- // Provider #2 (Google)
- $plain = 'users-password';
- $hash = 'EPKgfALpS9Tvgr/y1ki7ubY4AEGJeWL3teakrnmOacN4XGiyD00lkzEHgqCQ71wGxoi/zb7Y9a4orOtvMV3/Jw==';
- $salt = '56dFqW+kswqktw==';
- $saltSeparator = 'Bw==';
- $signerKey = 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==';
-
- $options = [ 'salt' => $salt, 'saltSeparator' => $saltSeparator, 'signerKey' => $signerKey ];
- $generatedHash = Auth::passwordHash($plain, 'scryptMod', $options);
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scryptMod', $options));
- $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scryptMod', $options));
- $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scryptMod', $options));
- }
-
- public function testUnknownAlgo()
- {
- $this->expectExceptionMessage('Hashing algorithm \'md8\' is not supported.');
-
- // Bcrypt - Cost 5
- $plain = 'whatIsMd8?!?';
- $generatedHash = Auth::passwordHash($plain, 'md8');
- $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md8'));
- }
-
- public function testPasswordGenerator(): void
- {
- $this->assertEquals(\mb_strlen(Auth::passwordGenerator()), 40);
- $this->assertEquals(\mb_strlen(Auth::passwordGenerator(5)), 10);
- }
-
- public function testTokenGenerator(): void
- {
- $this->assertEquals(\strlen(Auth::tokenGenerator()), 256);
- $this->assertEquals(\strlen(Auth::tokenGenerator(5)), 5);
- }
-
- public function testCodeGenerator(): void
- {
- $this->assertEquals(6, \strlen(Auth::codeGenerator()));
- $this->assertEquals(\mb_strlen(Auth::codeGenerator(256)), 256);
- $this->assertEquals(\mb_strlen(Auth::codeGenerator(10)), 10);
- $this->assertTrue(is_numeric(Auth::codeGenerator(5)));
- }
-
- public function testSessionVerify(): void
- {
- $expireTime1 = 60 * 60 * 24;
-
- $secret = 'secret1';
- $hash = Auth::hash($secret);
- $tokens1 = [
- new Document([
- '$id' => ID::custom('token1'),
- 'secret' => $hash,
- 'provider' => Auth::SESSION_PROVIDER_EMAIL,
- 'providerUid' => 'test@example.com',
- 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
- ]),
- new Document([
- '$id' => ID::custom('token2'),
- 'secret' => 'secret2',
- 'provider' => Auth::SESSION_PROVIDER_EMAIL,
- 'providerUid' => 'test@example.com',
- 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
- ]),
- ];
-
- $expireTime2 = -60 * 60 * 24;
-
- $tokens2 = [
- new Document([ // Correct secret and type time, wrong expire time
- '$id' => ID::custom('token1'),
- 'secret' => $hash,
- 'provider' => Auth::SESSION_PROVIDER_EMAIL,
- 'providerUid' => 'test@example.com',
- 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
- ]),
- new Document([
- '$id' => ID::custom('token2'),
- 'secret' => 'secret2',
- 'provider' => Auth::SESSION_PROVIDER_EMAIL,
- 'providerUid' => 'test@example.com',
- 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
- ]),
- ];
-
- $this->assertEquals(Auth::sessionVerify($tokens1, $secret), 'token1');
- $this->assertEquals(Auth::sessionVerify($tokens1, 'false-secret'), false);
- $this->assertEquals(Auth::sessionVerify($tokens2, $secret), false);
- $this->assertEquals(Auth::sessionVerify($tokens2, 'false-secret'), false);
- }
-
- public function testTokenVerify(): void
- {
- $secret = 'secret1';
- $hash = Auth::hash($secret);
- $tokens1 = [
- new Document([
- '$id' => ID::custom('token1'),
- 'type' => Auth::TOKEN_TYPE_RECOVERY,
- 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
- 'secret' => $hash,
- ]),
- new Document([
- '$id' => ID::custom('token2'),
- 'type' => Auth::TOKEN_TYPE_RECOVERY,
- 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
- 'secret' => 'secret2',
- ]),
- ];
-
- $tokens2 = [
- new Document([ // Correct secret and type time, wrong expire time
- '$id' => ID::custom('token1'),
- 'type' => Auth::TOKEN_TYPE_RECOVERY,
- 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
- 'secret' => $hash,
- ]),
- new Document([
- '$id' => ID::custom('token2'),
- 'type' => Auth::TOKEN_TYPE_RECOVERY,
- 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
- 'secret' => 'secret2',
- ]),
- ];
-
- $tokens3 = [ // Correct secret and expire time, wrong type
- new Document([
- '$id' => ID::custom('token1'),
- 'type' => Auth::TOKEN_TYPE_INVITE,
- 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
- 'secret' => $hash,
- ]),
- new Document([
- '$id' => ID::custom('token2'),
- 'type' => Auth::TOKEN_TYPE_RECOVERY,
- 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
- 'secret' => 'secret2',
- ]),
- ];
-
- $this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), $tokens1[0]);
- $this->assertEquals(Auth::tokenVerify($tokens1, null, $secret), $tokens1[0]);
- $this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
- $this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
- $this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
- $this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
- $this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
- }
-
- public function testIsPrivilegedUser(): void
- {
- $this->assertEquals(false, Auth::isPrivilegedUser([]));
- $this->assertEquals(false, Auth::isPrivilegedUser([Role::guests()->toString()]));
- $this->assertEquals(false, Auth::isPrivilegedUser([Role::users()->toString()]));
- $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_ADMIN]));
- $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_DEVELOPER]));
- $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER]));
- $this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS]));
- $this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_SYSTEM]));
-
- $this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
- $this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
- $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
- $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
- }
-
- public function testIsAppUser(): void
- {
- $this->assertEquals(false, Auth::isAppUser([]));
- $this->assertEquals(false, Auth::isAppUser([Role::guests()->toString()]));
- $this->assertEquals(false, Auth::isAppUser([Role::users()->toString()]));
- $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_ADMIN]));
- $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_DEVELOPER]));
- $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER]));
- $this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS]));
- $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_SYSTEM]));
-
- $this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
- $this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
- $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
- $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
- }
-
- public function testGuestRoles(): void
- {
- $user = new Document([
- '$id' => ''
- ]);
-
- $roles = Auth::getRoles($user);
- $this->assertCount(1, $roles);
- $this->assertContains(Role::guests()->toString(), $roles);
- }
-
- public function testUserRoles(): void
- {
- $user = new Document([
- '$id' => ID::custom('123'),
- 'labels' => [
- 'vip',
- 'admin'
- ],
- 'emailVerification' => true,
- 'phoneVerification' => true,
- 'memberships' => [
- [
- '$id' => ID::custom('456'),
- 'teamId' => ID::custom('abc'),
- 'confirm' => true,
- 'roles' => [
- 'administrator',
- 'moderator'
- ]
- ],
- [
- '$id' => ID::custom('abc'),
- 'teamId' => ID::custom('def'),
- 'confirm' => true,
- 'roles' => [
- 'guest'
- ]
- ]
- ]
- ]);
-
- $roles = Auth::getRoles($user);
-
- $this->assertCount(13, $roles);
- $this->assertContains(Role::users()->toString(), $roles);
- $this->assertContains(Role::user(ID::custom('123'))->toString(), $roles);
- $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
- $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
- $this->assertContains(Role::member(ID::custom('456'))->toString(), $roles);
- $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
- $this->assertContains('label:vip', $roles);
- $this->assertContains('label:admin', $roles);
-
- // Disable all verification
- $user['emailVerification'] = false;
- $user['phoneVerification'] = false;
-
- $roles = Auth::getRoles($user);
- $this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles);
- $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles);
-
- // Enable single verification type
- $user['emailVerification'] = true;
-
- $roles = Auth::getRoles($user);
- $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
- $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
- }
-
- public function testPrivilegedUserRoles(): void
- {
- Authorization::setRole(Auth::USER_ROLE_OWNER);
- $user = new Document([
- '$id' => ID::custom('123'),
- 'emailVerification' => true,
- 'phoneVerification' => true,
- 'memberships' => [
- [
- '$id' => ID::custom('def'),
- 'teamId' => ID::custom('abc'),
- 'confirm' => true,
- 'roles' => [
- 'administrator',
- 'moderator'
- ]
- ],
- [
- '$id' => ID::custom('abc'),
- 'teamId' => ID::custom('def'),
- 'confirm' => true,
- 'roles' => [
- 'guest'
- ]
- ]
- ]
- ]);
-
- $roles = Auth::getRoles($user);
-
- $this->assertCount(7, $roles);
- $this->assertNotContains(Role::users()->toString(), $roles);
- $this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles);
- $this->assertNotContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
- $this->assertNotContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
- $this->assertContains(Role::member(ID::custom('def'))->toString(), $roles);
- $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
- }
-
- public function testAppUserRoles(): void
- {
- Authorization::setRole(Auth::USER_ROLE_APPS);
- $user = new Document([
- '$id' => ID::custom('123'),
- 'memberships' => [
- [
- '$id' => ID::custom('def'),
- 'teamId' => ID::custom('abc'),
- 'confirm' => true,
- 'roles' => [
- 'administrator',
- 'moderator'
- ]
- ],
- [
- '$id' => ID::custom('abc'),
- 'teamId' => ID::custom('def'),
- 'confirm' => true,
- 'roles' => [
- 'guest'
- ]
- ]
- ]
- ]);
-
- $roles = Auth::getRoles($user);
-
- $this->assertCount(7, $roles);
- $this->assertNotContains(Role::users()->toString(), $roles);
- $this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
- $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
- $this->assertContains(Role::member(ID::custom('def'))->toString(), $roles);
- $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
- }
-}
diff --git a/tests/unit/Auth/KeyTest.php b/tests/unit/Auth/KeyTest.php
index 8ae2114697..920608e82f 100644
--- a/tests/unit/Auth/KeyTest.php
+++ b/tests/unit/Auth/KeyTest.php
@@ -3,8 +3,8 @@
namespace Tests\Unit\Auth;
use Ahc\Jwt\JWT;
-use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
+use Appwrite\Utopia\Database\Documents\User;
use PHPUnit\Framework\TestCase;
use Utopia\Config\Config;
use Utopia\Database\Document;
@@ -21,7 +21,7 @@ class KeyTest extends TestCase
'collections.read',
'documents.read',
];
- $roleScopes = Config::getParam('roles', [])[Auth::USER_ROLE_APPS]['scopes'];
+ $roleScopes = Config::getParam('roles', [])[User::ROLE_APPS]['scopes'];
$key = static::generateKey($projectId, $usage, $scopes);
$project = new Document(['$id' => $projectId,]);
@@ -29,7 +29,7 @@ class KeyTest extends TestCase
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
- $this->assertEquals(Auth::USER_ROLE_APPS, $decoded->getRole());
+ $this->assertEquals(User::ROLE_APPS, $decoded->getRole());
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
}
diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php
index 8ba0374093..42e433568f 100644
--- a/tests/unit/Messaging/MessagingChannelsTest.php
+++ b/tests/unit/Messaging/MessagingChannelsTest.php
@@ -2,10 +2,9 @@
namespace Tests\Unit\Messaging;
-use Appwrite\Auth\Auth;
use Appwrite\Messaging\Adapter\Realtime;
+use Appwrite\Utopia\Database\Documents\User;
use PHPUnit\Framework\TestCase;
-use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
@@ -50,7 +49,7 @@ class MessagingChannelsTest extends TestCase
*/
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
foreach ($this->allChannels as $index => $channel) {
- $user = new Document([
+ $user = new User([
'$id' => ID::custom('user' . $this->connectionsCount),
'memberships' => [
[
@@ -59,14 +58,14 @@ class MessagingChannelsTest extends TestCase
'confirm' => true,
'roles' => [
empty($index % 2)
- ? Auth::USER_ROLE_ADMIN
+ ? User::ROLE_ADMIN
: 'member',
]
]
]
]);
- $roles = Auth::getRoles($user);
+ $roles = $user->getRoles();
$parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId());
@@ -86,11 +85,11 @@ class MessagingChannelsTest extends TestCase
*/
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
foreach ($this->allChannels as $index => $channel) {
- $user = new Document([
+ $user = new User([
'$id' => ''
]);
- $roles = Auth::getRoles($user);
+ $roles = $user->getRoles();
$parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId());
@@ -294,7 +293,7 @@ class MessagingChannelsTest extends TestCase
}
$role = empty($index % 2)
- ? Auth::USER_ROLE_ADMIN
+ ? User::ROLE_ADMIN
: 'member';
$permissions = [
diff --git a/tests/unit/Network/CorsTest.php b/tests/unit/Network/CorsTest.php
new file mode 100644
index 0000000000..521bf21f1e
--- /dev/null
+++ b/tests/unit/Network/CorsTest.php
@@ -0,0 +1,150 @@
+expectException(InvalidArgumentException::class);
+
+ new Cors(
+ allowedHosts: ['*'],
+ allowedMethods: ['GET'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: true
+ );
+ }
+
+ public function testWildcardAllowsAnyOrigin(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['*'],
+ allowedMethods: ['GET'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: false
+ );
+
+ $result = $cors->headers('https://foo.com');
+
+ $this->assertSame('https://foo.com', $result[Cors::HEADER_ALLOW_ORIGIN]);
+ }
+
+ public function testEmptyOriginReturnsStaticHeadersOnly(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['example.com'],
+ allowedMethods: ['GET'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: false
+ );
+
+ $result = $cors->headers('');
+
+ $this->assertArrayNotHasKey(Cors::HEADER_ALLOW_ORIGIN, $result);
+ $this->assertSame('false', $result[Cors::HEADER_ALLOW_CREDENTIALS]);
+ $this->assertSame('GET', $result[Cors::HEADER_ALLOW_METHODS]);
+ }
+
+ public function testInvalidOriginReturnsStaticHeadersOnly(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['example.com'],
+ allowedMethods: ['GET'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: false
+ );
+
+ $result = $cors->headers('%%%not-a-url%%%');
+
+ $this->assertArrayNotHasKey(Cors::HEADER_ALLOW_ORIGIN, $result);
+ }
+
+ public function testUnlistedOriginReturnsStaticHeadersOnly(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['allowed.com'],
+ allowedMethods: ['GET'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: false
+ );
+
+ $result = $cors->headers('https://forbidden.com');
+
+ $this->assertArrayNotHasKey(Cors::HEADER_ALLOW_ORIGIN, $result);
+ }
+
+ public function testAllowedOriginIsReturned(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['example.com'],
+ allowedMethods: ['POST'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: true
+ );
+
+ $result = $cors->headers('https://example.com');
+
+ $this->assertSame('https://example.com', $result[Cors::HEADER_ALLOW_ORIGIN]);
+ }
+
+ public function testOriginIsLowercasedForMatching(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['example.com'],
+ allowedMethods: ['GET'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: false
+ );
+
+ $result = $cors->headers('HTTPS://EXAMPLE.COM');
+
+ // Lowercase logic is in the class
+ $this->assertSame('https://example.com', $result[Cors::HEADER_ALLOW_ORIGIN]);
+ }
+
+ public function testHeaderFormatting(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['example.com'],
+ allowedMethods: ['GET', 'POST'],
+ allowedHeaders: ['X-A', 'X-B'],
+ exposedHeaders: ['E1', 'E2'],
+ allowCredentials: true
+ );
+
+ $result = $cors->headers('https://example.com');
+
+ $this->assertSame('GET, POST', $result[Cors::HEADER_ALLOW_METHODS]);
+ $this->assertSame('X-A, X-B', $result[Cors::HEADER_ALLOW_HEADERS]);
+ $this->assertSame('E1, E2', $result[Cors::HEADER_EXPOSE_HEADERS]);
+ $this->assertSame('true', $result[Cors::HEADER_ALLOW_CREDENTIALS]);
+ }
+
+ public function testMaxAgeIncluded(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['example.com'],
+ allowedMethods: ['GET'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: false,
+ maxAge: 999
+ );
+
+ $result = $cors->headers('https://example.com');
+
+ $this->assertSame(999, $result[Cors::HEADER_MAX_AGE]);
+ }
+}
diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php
index c3e819e7dc..4611e00f4d 100644
--- a/tests/unit/Network/Validators/DNSTest.php
+++ b/tests/unit/Network/Validators/DNSTest.php
@@ -5,30 +5,20 @@ namespace Tests\Unit\Network\Validators;
use Appwrite\Network\Validator\DNS;
use Appwrite\Tests\Retry;
use PHPUnit\Framework\TestCase;
+use Utopia\DNS\Message\Record;
-/*
-DNS Setup (on Appwrite Labs digital ocean team, network tab):
-
-certainly.caa.appwrite.org: CAA 0 issue "certainly.com"
-certainly-full.caa.appwrite.org: CAA 128 issuewild "certainly.com;account=123456;validationmethods=dns-01"
-letsencrypt.certainly.caa.appwrite.org: CAA 0 issue "letsencrypt.org"
-
-*/
-
+/**
+ * DNS Setup (on Appwrite Labs digital ocean team, network tab):
+ *
+ * certainly.caa.appwrite.org: CAA 0 issue "certainly.com"
+ * certainly-full.caa.appwrite.org: CAA 128 issuewild "certainly.com;account=123456;validationmethods=dns-01"
+ * letsencrypt.certainly.caa.appwrite.org: CAA 0 issue "letsencrypt.org"
+ */
class DNSTest extends TestCase
{
- public function setUp(): void
- {
-
- }
-
- public function tearDown(): void
- {
- }
-
public function testCNAME(): void
{
- $validator = new DNS('appwrite.io', DNS::RECORD_CNAME);
+ $validator = new DNS('appwrite.io', Record::TYPE_CNAME);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
@@ -39,7 +29,7 @@ class DNSTest extends TestCase
public function testA(): void
{
// IPv4 for documentation purposes
- $validator = new DNS('203.0.113.1', DNS::RECORD_A);
+ $validator = new DNS('203.0.113.1', Record::TYPE_A);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
@@ -50,7 +40,7 @@ class DNSTest extends TestCase
public function testAAAA(): void
{
// IPv6 for documentation purposes
- $validator = new DNS('2001:db8::1', DNS::RECORD_AAAA);
+ $validator = new DNS('2001:db8::1', Record::TYPE_AAAA);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
@@ -61,8 +51,10 @@ class DNSTest extends TestCase
#[Retry(count: 5)]
public function testCAA(): void
{
- $certainly = new DNS('certainly.com', DNS::RECORD_CAA, 'ns1.digitalocean.com');
- $letsencrypt = new DNS('letsencrypt.org', DNS::RECORD_CAA, 'ns1.digitalocean.com');
+ $digitalOceanIp = '172.64.52.210'; // ping ns1.digitalocean.com
+
+ $certainly = new DNS('certainly.com', Record::TYPE_CAA, $digitalOceanIp);
+ $letsencrypt = new DNS('letsencrypt.org', Record::TYPE_CAA, $digitalOceanIp);
// No CAA record succeeds on main domain & subdomains for any issuer
$this->assertEquals($certainly->isValid('caa.appwrite.org'), true);
@@ -78,11 +70,11 @@ class DNSTest extends TestCase
$this->assertEquals($letsencrypt->isValid('certainly-full.caa.appwrite.org'), false);
// Custom flags&tag are not allowed if validator includes specific flags&tag
- $certainlyFull = new DNS('0 issue "certainly.com"', DNS::RECORD_CAA);
+ $certainlyFull = new DNS('0 issue "certainly.com"', Record::TYPE_CAA);
$this->assertEquals($certainlyFull->isValid('certainly-full.caa.appwrite.org'), false);
// Custom flags&tag still allows if they match exactly
- $certainlyFull = new DNS('128 issuewild "certainly.com;account=123456;validationmethods=dns-01"', DNS::RECORD_CAA);
+ $certainlyFull = new DNS('128 issuewild "certainly.com;account=123456;validationmethods=dns-01"', Record::TYPE_CAA);
$this->assertEquals($certainlyFull->isValid('certainly-full.caa.appwrite.org'), true);
// Certainly CAA allows Certainly, but not LetsEncrypt; Same for subdomains
diff --git a/tests/unit/Network/Validators/OriginTest.php b/tests/unit/Network/Validators/OriginTest.php
index d312f8c5a5..a4c235f755 100644
--- a/tests/unit/Network/Validators/OriginTest.php
+++ b/tests/unit/Network/Validators/OriginTest.php
@@ -2,53 +2,17 @@
namespace Tests\Unit\Network\Validators;
-use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use PHPUnit\Framework\TestCase;
-use Utopia\Database\Helpers\ID;
class OriginTest extends TestCase
{
public function testValues(): void
{
- $validator = new Origin([
- [
- '$collection' => ID::custom('platforms'),
- 'name' => 'Production',
- 'type' => Platform::TYPE_WEB,
- 'hostname' => 'appwrite.io',
- ],
- [
- '$collection' => ID::custom('platforms'),
- 'name' => 'Development',
- 'type' => Platform::TYPE_WEB,
- 'hostname' => 'appwrite.test',
- ],
- [
- '$collection' => ID::custom('platforms'),
- 'name' => 'Localhost',
- 'type' => Platform::TYPE_WEB,
- 'hostname' => 'localhost',
- ],
- [
- '$collection' => ID::custom('platforms'),
- 'name' => 'Flutter',
- 'type' => Platform::TYPE_FLUTTER_WEB,
- 'hostname' => 'appwrite.flutter',
- ],
- [
- '$collection' => ID::custom('platforms'),
- 'name' => 'Expo',
- 'type' => Platform::TYPE_SCHEME,
- 'key' => 'exp',
- ],
- [
- '$collection' => ID::custom('platforms'),
- 'name' => 'Appwrite Callback',
- 'type' => Platform::TYPE_SCHEME,
- 'key' => 'appwrite-callback-123',
- ],
- ]);
+ $validator = new Origin(
+ allowedHostnames: ['appwrite.io', 'appwrite.test', 'localhost', 'appwrite.flutter'],
+ allowedSchemes: ['exp', 'appwrite-callback-123']
+ );
$this->assertEquals(false, $validator->isValid(''));
$this->assertEquals(false, $validator->isValid('/'));
diff --git a/tests/unit/Platform/Modules/Compute/Validator/SpecificationTest.php b/tests/unit/Platform/Modules/Compute/Validator/SpecificationTest.php
new file mode 100644
index 0000000000..0505494e17
--- /dev/null
+++ b/tests/unit/Platform/Modules/Compute/Validator/SpecificationTest.php
@@ -0,0 +1,75 @@
+specifications = Config::getParam('specifications', []);
+ }
+
+ public function testGetAllowedSpecificationsNoLimits(): void
+ {
+ $validator = new Specification(
+ plan: [],
+ specifications: $this->specifications,
+ maxCpus: 0,
+ maxMemory: 0
+ );
+
+ $allowed = $validator->getAllowedSpecifications();
+ $this->assertCount(count($this->specifications), $allowed);
+ $this->assertEquals(
+ $this->specifications[array_key_last($this->specifications)]['slug'],
+ $allowed[array_key_last($allowed)]
+ );
+ }
+
+ public function testGetAllowedSpecificationsWithMaxCpusAndMemory(): void
+ {
+ $validator = new Specification(
+ plan: [],
+ specifications: $this->specifications,
+ maxCpus: 2,
+ maxMemory: 2048
+ );
+
+ $allowed = $validator->getAllowedSpecifications();
+ $this->assertCount(4, $allowed);
+ $this->assertEquals(
+ SpecificationConstants::S_2VCPU_2GB,
+ $allowed[array_key_last($allowed)]
+ );
+ }
+
+ public function testGetAllowedSpecificationsWithPlanLimits(): void
+ {
+ $plan = [
+ 'runtimeSpecifications' => [
+ SpecificationConstants::S_05VCPU_512MB,
+ SpecificationConstants::S_1VCPU_512MB
+ ]
+ ];
+ $validator = new Specification(
+ plan: $plan,
+ specifications: $this->specifications,
+ maxCpus: 0,
+ maxMemory: 0
+ );
+
+ $allowed = $validator->getAllowedSpecifications();
+ $this->assertCount(2, $allowed);
+ $this->assertContains(SpecificationConstants::S_05VCPU_512MB, $allowed);
+ $this->assertContains(SpecificationConstants::S_1VCPU_512MB, $allowed);
+ }
+}
diff --git a/tests/unit/Utopia/Database/Documents/UserTest.php b/tests/unit/Utopia/Database/Documents/UserTest.php
new file mode 100644
index 0000000000..4675e8d73f
--- /dev/null
+++ b/tests/unit/Utopia/Database/Documents/UserTest.php
@@ -0,0 +1,352 @@
+toString());
+ }
+
+ public function testSessionVerify(): void
+ {
+ $proofForToken = new Token();
+ $expireTime1 = 60 * 60 * 24;
+
+ $secret = 'secret1';
+ $hash = $proofForToken->hash($secret);
+ $tokens1 = [
+ new Document([
+ '$id' => ID::custom('token1'),
+ 'secret' => $hash,
+ 'provider' => SESSION_PROVIDER_EMAIL,
+ 'providerUid' => 'test@example.com',
+ 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
+ ]),
+ new Document([
+ '$id' => ID::custom('token2'),
+ 'secret' => 'secret2',
+ 'provider' => SESSION_PROVIDER_EMAIL,
+ 'providerUid' => 'test@example.com',
+ 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
+ ]),
+ ];
+
+ $expireTime2 = -60 * 60 * 24;
+
+ $tokens2 = [
+ new Document([ // Correct secret and type time, wrong expire time
+ '$id' => ID::custom('token1'),
+ 'secret' => $hash,
+ 'provider' => SESSION_PROVIDER_EMAIL,
+ 'providerUid' => 'test@example.com',
+ 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
+ ]),
+ new Document([
+ '$id' => ID::custom('token2'),
+ 'secret' => 'secret2',
+ 'provider' => SESSION_PROVIDER_EMAIL,
+ 'providerUid' => 'test@example.com',
+ 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
+ ]),
+ ];
+
+ $user1 = new User([
+ '$id' => ID::custom('user1'),
+ 'sessions' => $tokens1,
+
+ ]);
+
+ $user2 = new User([
+ '$id' => ID::custom('user2'),
+ 'sessions' => $tokens2,
+ ]);
+
+ $this->assertEquals('token1', $user1->sessionVerify($secret, $proofForToken));
+ $this->assertEquals($user1->sessionVerify('false-secret', $proofForToken), false);
+ $this->assertEquals($user2->sessionVerify($secret, $proofForToken), false);
+ $this->assertEquals($user2->sessionVerify('false-secret', $proofForToken), false);
+ }
+
+ public function testTokenVerify(): void
+ {
+ $proofForToken = new Token();
+ $secret = 'secret1';
+ $hash = $proofForToken->hash($secret);
+ $tokens1 = [
+ new Document([
+ '$id' => ID::custom('token1'),
+ 'type' => TOKEN_TYPE_RECOVERY,
+ 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
+ 'secret' => $hash,
+ ]),
+ new Document([
+ '$id' => ID::custom('token2'),
+ 'type' => TOKEN_TYPE_RECOVERY,
+ 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
+ 'secret' => 'secret2',
+ ]),
+ ];
+
+ $tokens2 = [
+ new Document([ // Correct secret and type time, wrong expire time
+ '$id' => ID::custom('token1'),
+ 'type' => TOKEN_TYPE_RECOVERY,
+ 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
+ 'secret' => $hash,
+ ]),
+ new Document([
+ '$id' => ID::custom('token2'),
+ 'type' => TOKEN_TYPE_RECOVERY,
+ 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
+ 'secret' => 'secret2',
+ ]),
+ ];
+
+ $tokens3 = [ // Correct secret and expire time, wrong type
+ new Document([
+ '$id' => ID::custom('token1'),
+ 'type' => TOKEN_TYPE_INVITE,
+ 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
+ 'secret' => $hash,
+ ]),
+ new Document([
+ '$id' => ID::custom('token2'),
+ 'type' => TOKEN_TYPE_RECOVERY,
+ 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
+ 'secret' => 'secret2',
+ ]),
+ ];
+
+ $user1 = new User([
+ '$id' => ID::custom('user1'),
+ 'tokens' => $tokens1,
+ ]);
+
+ $user2 = new User([
+ '$id' => ID::custom('user2'),
+ 'tokens' => $tokens2,
+ ]);
+
+ $user3 = new User([
+ '$id' => ID::custom('user3'),
+ 'tokens' => $tokens3,
+ ]);
+
+ $this->assertEquals($user1->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), $tokens1[0]);
+ $this->assertEquals($user1->tokenVerify(null, $secret, $proofForToken), $tokens1[0]);
+ $this->assertEquals($user1->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false);
+ $this->assertEquals($user2->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), false);
+ $this->assertEquals($user2->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false);
+ $this->assertEquals($user3->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), false);
+ $this->assertEquals($user3->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false);
+ }
+
+ public function testIsPrivilegedUser(): void
+ {
+ $this->assertEquals(false, User::isPrivileged([]));
+ $this->assertEquals(false, User::isPrivileged([Role::guests()->toString()]));
+ $this->assertEquals(false, User::isPrivileged([Role::users()->toString()]));
+ $this->assertEquals(true, User::isPrivileged([User::ROLE_ADMIN]));
+ $this->assertEquals(true, User::isPrivileged([User::ROLE_DEVELOPER]));
+ $this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER]));
+ $this->assertEquals(false, User::isPrivileged([User::ROLE_APPS]));
+ $this->assertEquals(false, User::isPrivileged([User::ROLE_SYSTEM]));
+
+ $this->assertEquals(false, User::isPrivileged([User::ROLE_APPS, User::ROLE_APPS]));
+ $this->assertEquals(false, User::isPrivileged([User::ROLE_APPS, Role::guests()->toString()]));
+ $this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER, Role::guests()->toString()]));
+ $this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER]));
+ }
+
+ public function testIsAppUser(): void
+ {
+ $this->assertEquals(false, User::isApp([]));
+ $this->assertEquals(false, User::isApp([Role::guests()->toString()]));
+ $this->assertEquals(false, User::isApp([Role::users()->toString()]));
+ $this->assertEquals(false, User::isApp([User::ROLE_ADMIN]));
+ $this->assertEquals(false, User::isApp([User::ROLE_DEVELOPER]));
+ $this->assertEquals(false, User::isApp([User::ROLE_OWNER]));
+ $this->assertEquals(true, User::isApp([User::ROLE_APPS]));
+ $this->assertEquals(false, User::isApp([User::ROLE_SYSTEM]));
+
+ $this->assertEquals(true, User::isApp([User::ROLE_APPS, User::ROLE_APPS]));
+ $this->assertEquals(true, User::isApp([User::ROLE_APPS, Role::guests()->toString()]));
+ $this->assertEquals(false, User::isApp([User::ROLE_OWNER, Role::guests()->toString()]));
+ $this->assertEquals(false, User::isApp([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER]));
+ }
+
+ public function testGuestRoles(): void
+ {
+ $user = new User([
+ '$id' => ''
+ ]);
+
+ $roles = $user->getRoles();
+ $this->assertCount(1, $roles);
+ $this->assertContains(Role::guests()->toString(), $roles);
+ }
+
+ public function testUserRoles(): void
+ {
+ $user = new User([
+ '$id' => ID::custom('123'),
+ 'labels' => [
+ 'vip',
+ 'admin'
+ ],
+ 'emailVerification' => true,
+ 'phoneVerification' => true,
+ 'memberships' => [
+ [
+ '$id' => ID::custom('456'),
+ 'teamId' => ID::custom('abc'),
+ 'confirm' => true,
+ 'roles' => [
+ 'administrator',
+ 'moderator'
+ ]
+ ],
+ [
+ '$id' => ID::custom('abc'),
+ 'teamId' => ID::custom('def'),
+ 'confirm' => true,
+ 'roles' => [
+ 'guest'
+ ]
+ ]
+ ]
+ ]);
+
+ $roles = $user->getRoles();
+
+ $this->assertCount(13, $roles);
+ $this->assertContains(Role::users()->toString(), $roles);
+ $this->assertContains(Role::user(ID::custom('123'))->toString(), $roles);
+ $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
+ $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
+ $this->assertContains(Role::member(ID::custom('456'))->toString(), $roles);
+ $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
+ $this->assertContains('label:vip', $roles);
+ $this->assertContains('label:admin', $roles);
+
+ // Disable all verification
+ $user['emailVerification'] = false;
+ $user['phoneVerification'] = false;
+
+ $roles = $user->getRoles();
+ $this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles);
+ $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles);
+
+ // Enable single verification type
+ $user['emailVerification'] = true;
+
+ $roles = $user->getRoles();
+ $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
+ $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
+ }
+
+ public function testPrivilegedUserRoles(): void
+ {
+ Authorization::setRole(User::ROLE_OWNER);
+ $user = new User([
+ '$id' => ID::custom('123'),
+ 'emailVerification' => true,
+ 'phoneVerification' => true,
+ 'memberships' => [
+ [
+ '$id' => ID::custom('def'),
+ 'teamId' => ID::custom('abc'),
+ 'confirm' => true,
+ 'roles' => [
+ 'administrator',
+ 'moderator'
+ ]
+ ],
+ [
+ '$id' => ID::custom('abc'),
+ 'teamId' => ID::custom('def'),
+ 'confirm' => true,
+ 'roles' => [
+ 'guest'
+ ]
+ ]
+ ]
+ ]);
+
+ $roles = $user->getRoles();
+
+ $this->assertCount(7, $roles);
+ $this->assertNotContains(Role::users()->toString(), $roles);
+ $this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles);
+ $this->assertNotContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
+ $this->assertNotContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
+ $this->assertContains(Role::member(ID::custom('def'))->toString(), $roles);
+ $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
+ }
+
+ public function testAppUserRoles(): void
+ {
+ Authorization::setRole(User::ROLE_APPS);
+ $user = new User([
+ '$id' => ID::custom('123'),
+ 'memberships' => [
+ [
+ '$id' => ID::custom('def'),
+ 'teamId' => ID::custom('abc'),
+ 'confirm' => true,
+ 'roles' => [
+ 'administrator',
+ 'moderator'
+ ]
+ ],
+ [
+ '$id' => ID::custom('abc'),
+ 'teamId' => ID::custom('def'),
+ 'confirm' => true,
+ 'roles' => [
+ 'guest'
+ ]
+ ]
+ ]
+ ]);
+
+ $roles = $user->getRoles();
+
+ $this->assertCount(7, $roles);
+ $this->assertNotContains(Role::users()->toString(), $roles);
+ $this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
+ $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
+ $this->assertContains(Role::member(ID::custom('def'))->toString(), $roles);
+ $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
+ }
+}