diff --git a/.env b/.env
index 4d7c038a6b..64fc7ef10f 100644
--- a/.env
+++ b/.env
@@ -21,13 +21,13 @@ _APP_OPTIONS_ROUTER_PROTECTION=disabled
_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_DNS=172.16.238.100 # CoreDNS
+_APP_DOMAIN=appwrite.test
_APP_CONSOLE_DOMAIN=localhost
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_SITES=sites.localhost
-_APP_DOMAIN_TARGET_CNAME=test.localhost
-_APP_DOMAIN_TARGET_A=127.0.0.1
+_APP_DOMAIN_TARGET_CNAME=cname.localhost
+_APP_DOMAIN_TARGET_A=203.0.0.1
_APP_DOMAIN_TARGET_AAAA=::1
_APP_DOMAIN_TARGET_CAA=digicert.com
_APP_RULES_FORMAT=md5
@@ -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,5 @@ _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
+_APP_TRUSTED_HEADERS=x-forwarded-for
\ No newline at end of file
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index c78156ca04..180eb5428d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -42,6 +42,7 @@ jobs:
with:
context: .
platforms: linux/amd64,linux/arm64
+ target: production
build-args: |
VERSION=${{ steps.meta.outputs.version }}
VITE_APPWRITE_GROWTH_ENDPOINT=https://growth.appwrite.io/v1
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 862d669466..5426f53583 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,10 +20,10 @@ jobs:
submodules: recursive
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -46,6 +46,7 @@ jobs:
with:
context: .
platforms: linux/amd64,linux/arm64
+ target: production
build-args: |
VERSION=${{ steps.meta.outputs.version }}
push: true
diff --git a/Dockerfile b/Dockerfile
index 2a3e176838..e146008222 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 base
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
@@ -38,9 +36,7 @@ COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
COPY ./app /usr/src/code/app
COPY ./public /usr/src/code/public
COPY ./bin /usr/local/bin
-COPY ./docs /usr/src/code/docs
COPY ./src /usr/src/code/src
-COPY ./dev /usr/src/code/dev
# Set Volumes
RUN mkdir -p /storage/uploads && \
@@ -92,14 +88,30 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/stats-resources && \
chmod +x /usr/local/bin/worker-stats-resources
-# Letsencrypt Permissions
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" = "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
+FROM base AS production
+
+RUN rm -rf /usr/src/code/app/config/specs && \
+ rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so && \
+ find /usr -name '*.a' -delete 2>/dev/null || true && \
+ find /usr -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
+ find /usr -name '*.pyc' -delete 2>/dev/null || true
+
+EXPOSE 80
+
+CMD [ "php", "app/http.php" ]
+
+FROM base AS development
+
+COPY ./docs /usr/src/code/docs
+COPY ./dev /usr/src/code/dev
+
+RUN if [ "$DEBUG" = "true" ]; then \
+ cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && \
+ mkdir -p /tmp/xdebug && \
+ apk add --update --no-cache openssh-client github-cli; \
+ fi
EXPOSE 80
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..7b65ab8fa5 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;
@@ -40,8 +41,6 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false));
// require controllers after overwriting runtimes
require_once __DIR__ . '/controllers/general.php';
-Authorization::disable();
-
CLI::setResource('register', fn () => $register);
CLI::setResource('cache', function ($pools) {
@@ -59,7 +58,13 @@ CLI::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
-CLI::setResource('dbForPlatform', function ($pools, $cache) {
+CLI::setResource('authorization', function () {
+ $authorization = new Authorization();
+ $authorization->disable();
+ return $authorization;
+}, []);
+
+CLI::setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -73,9 +78,11 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
+ ->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
+ $dbForPlatform->setDocumentType('users', User::class);
// Ensure tables exist
$collections = Config::getParam('collections', [])['console'];
@@ -97,7 +104,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
}
return $dbForPlatform;
-}, ['pools', 'cache']);
+}, ['pools', 'cache', 'authorization']);
CLI::setResource('console', function () {
return new Document(Config::getParam('console'));
@@ -108,10 +115,10 @@ CLI::setResource(
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
-CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
+CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
- return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
+ return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -144,6 +151,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
+
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -160,17 +168,18 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
}
$database
+ ->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
return $database;
};
-}, ['pools', 'dbForPlatform', 'cache']);
+}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
-CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
+CLI::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
- return function (?Document $project = null) use ($pools, $cache, $database) {
+ return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence());
return $database;
@@ -180,6 +189,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
+ ->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK)
@@ -192,7 +202,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
-}, ['pools', 'cache']);
+}, ['pools', 'cache', 'authorization']);
CLI::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
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/platform.php b/app/config/collections/platform.php
index b839e51622..d44d9b725c 100644
--- a/app/config/collections/platform.php
+++ b/app/config/collections/platform.php
@@ -1317,6 +1317,17 @@ return [
'array' => false,
'filters' => [],
],
+ [
+ '$id' => ID::custom('logs'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 1000000,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
],
'indexes' => [
[
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..81964b968f 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..7df5b8d1e6 100644
--- a/tests/unit/Messaging/MessagingChannelsTest.php
+++ b/tests/unit/Messaging/MessagingChannelsTest.php
@@ -2,12 +2,12 @@
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;
+use Utopia\Database\Validator\Authorization;
class MessagingChannelsTest extends TestCase
{
@@ -34,6 +34,19 @@ class MessagingChannelsTest extends TestCase
'functions.1',
];
+
+ private $authorization;
+
+ public function getAuthorization(): Authorization
+ {
+ if (isset($this->authorization)) {
+ return $this->authorization;
+ }
+
+ $this->authorization = new Authorization();
+ return $this->authorization;
+ }
+
public function setUp(): void
{
/**
@@ -50,7 +63,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 +72,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($this->getAuthorization());
$parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId());
@@ -86,11 +99,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($this->getAuthorization());
$parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId());
@@ -294,7 +307,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..986e48ebb5
--- /dev/null
+++ b/tests/unit/Network/CorsTest.php
@@ -0,0 +1,165 @@
+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 testSubdomainWildcardAllowsAnySubdomain(): void
+ {
+ $cors = new Cors(
+ allowedHosts: ['*.example.com'],
+ allowedMethods: ['GET'],
+ allowedHeaders: ['X-Test'],
+ exposedHeaders: [],
+ allowCredentials: false
+ );
+
+ $result = $cors->headers('https://foo.example.com');
+
+ $this->assertSame('https://foo.example.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..6e4a78022f 100644
--- a/tests/unit/Network/Validators/DNSTest.php
+++ b/tests/unit/Network/Validators/DNSTest.php
@@ -3,106 +3,51 @@
namespace Tests\Unit\Network\Validators;
use Appwrite\Network\Validator\DNS;
-use Appwrite\Tests\Retry;
use PHPUnit\Framework\TestCase;
-
-/*
-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"
-
-*/
+use Utopia\DNS\Message\Record;
class DNSTest extends TestCase
{
- public function setUp(): void
+ public function testSingleDNSServer(): void
{
+ $validator = new DNS('appwrite.io', Record::TYPE_CNAME, ['8.8.8.8']);
+ $this->assertEquals(false, $validator->isValid(''));
+ $this->assertEquals(false, $validator->isValid(null));
+ $this->assertEquals('string', $validator->getType());
}
- public function tearDown(): void
+ public function testMultipleDNSServers(): void
{
+ $validator = new DNS('appwrite.io', Record::TYPE_CNAME, ['8.8.8.8', '1.1.1.1']);
+
+ $this->assertEquals(false, $validator->isValid(''));
+ $this->assertEquals(false, $validator->isValid(null));
+ $this->assertEquals('string', $validator->getType());
}
- public function testCNAME(): void
+ public function testValidationFailure(): void
{
- $validator = new DNS('appwrite.io', DNS::RECORD_CNAME);
- $this->assertEquals($validator->isValid(''), false);
- $this->assertEquals($validator->isValid(null), false);
- $this->assertEquals($validator->isValid(false), false);
- $this->assertEquals($validator->isValid('cname-unit-test.appwrite.org'), true);
- $this->assertEquals($validator->isValid('test1.appwrite.org'), false);
+ $validator = new DNS('invalid-target.example.com', Record::TYPE_CNAME, ['8.8.8.8', '1.1.1.1']);
+
+ $result = $validator->isValid('nonexistent-domain-' . \uniqid() . '.com');
+
+ $this->assertEquals(false, $result);
+ $this->assertIsInt($validator->count);
+ $this->assertIsString($validator->value);
+ $this->assertIsArray($validator->records);
+ $this->assertIsString($validator->getDescription());
}
- public function testA(): void
+ public function testCoreDNSFailure(): void
{
- // IPv4 for documentation purposes
- $validator = new DNS('203.0.113.1', DNS::RECORD_A);
- $this->assertEquals($validator->isValid(''), false);
- $this->assertEquals($validator->isValid(null), false);
- $this->assertEquals($validator->isValid(false), false);
- $this->assertEquals($validator->isValid('a-unit-test.appwrite.org'), true);
- $this->assertEquals($validator->isValid('test1.appwrite.org'), false);
- }
+ // CoreDNS is configured to return cname.localhost. for stage.webapp.com
+ $validator = new DNS('cname.localhost.', Record::TYPE_CNAME, ['172.16.238.100', '8.8.8.8']);
- public function testAAAA(): void
- {
- // IPv6 for documentation purposes
- $validator = new DNS('2001:db8::1', DNS::RECORD_AAAA);
- $this->assertEquals($validator->isValid(''), false);
- $this->assertEquals($validator->isValid(null), false);
- $this->assertEquals($validator->isValid(false), false);
- $this->assertEquals($validator->isValid('aaaa-unit-test.appwrite.org'), true);
- $this->assertEquals($validator->isValid('test1.appwrite.org'), false);
- }
+ $result = $validator->isValid('stage.webapp.com');
+ $this->assertEquals(false, $result);
- #[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');
-
- // No CAA record succeeds on main domain & subdomains for any issuer
- $this->assertEquals($certainly->isValid('caa.appwrite.org'), true);
- $this->assertEquals($certainly->isValid('sub.caa.appwrite.org'), true);
- $this->assertEquals($certainly->isValid('sub.sub.caa.appwrite.org'), true);
-
- $this->assertEquals($letsencrypt->isValid('caa.appwrite.org'), true);
- $this->assertEquals($letsencrypt->isValid('sub.caa.appwrite.org'), true);
- $this->assertEquals($letsencrypt->isValid('sub.sub.caa.appwrite.org'), true);
-
- // Custom flags and tag is allowed, but only for Certainly
- $this->assertEquals($certainly->isValid('certainly-full.caa.appwrite.org'), true);
- $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);
- $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);
- $this->assertEquals($certainlyFull->isValid('certainly-full.caa.appwrite.org'), true);
-
- // Certainly CAA allows Certainly, but not LetsEncrypt; Same for subdomains
- $this->assertEquals($certainly->isValid('certainly.caa.appwrite.org'), true);
- $this->assertEquals($letsencrypt->isValid('certainly.caa.appwrite.org'), false);
-
- $this->assertEquals($certainly->isValid('sub.certainly.caa.appwrite.org'), true);
- $this->assertEquals($letsencrypt->isValid('sub.certainly.caa.appwrite.org'), false);
-
- $this->assertEquals($certainly->isValid('sub.sub.certainly.caa.appwrite.org'), true);
- $this->assertEquals($letsencrypt->isValid('sub.sub.certainly.caa.appwrite.org'), false);
-
- // LetsEncrypt CAA on subdomain with parent allowing Certainly. Only LetsEncrypt is allowed; Same for subdomains
- $this->assertEquals($certainly->isValid('letsencrypt.certainly.caa.appwrite.org'), false);
- $this->assertEquals($letsencrypt->isValid('letsencrypt.certainly.caa.appwrite.org'), true);
-
- $this->assertEquals($certainly->isValid('sub.letsencrypt.certainly.caa.appwrite.org'), false);
- $this->assertEquals($letsencrypt->isValid('sub.letsencrypt.certainly.caa.appwrite.org'), true);
-
- $this->assertEquals($certainly->isValid('sub.sub.letsencrypt.certainly.caa.appwrite.org'), false);
- $this->assertEquals($letsencrypt->isValid('sub.sub.letsencrypt.certainly.caa.appwrite.org'), true);
+ $result = $validator->isValid('stage-wrong-cname.webapp.com');
+ $this->assertEquals(false, $result);
}
}
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..d5706e7bec
--- /dev/null
+++ b/tests/unit/Utopia/Database/Documents/UserTest.php
@@ -0,0 +1,363 @@
+authorization)) {
+ return $this->authorization;
+ }
+
+ $this->authorization = new Authorization();
+ return $this->authorization;
+ }
+
+ /**
+ * Reset Roles
+ */
+ public function tearDown(): void
+ {
+ $this->getAuthorization()->cleanRoles();
+ $this->getAuthorization()->addRole(Role::any()->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->getAuthorization());
+ $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->getAuthorization());
+
+ $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->getAuthorization());
+ $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->getAuthorization());
+ $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
+ {
+ $this->getAuthorization()->addRole(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->getAuthorization());
+
+ $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
+ {
+ $this->getAuthorization()->addRole(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->getAuthorization());
+
+ $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);
+ }
+}