mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
d2230f8fe7
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:
- `app/controllers/general.php`: path-traversal guard was negating
`\substr(...)` before the strict comparison (`!\substr(...) === $base`
was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
`DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
result as an array with `deviceName/deviceBrand/deviceModel` keys.
Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
key was checking `$resourceKey === 'functions'` when `$resourceKey`
is `'functionId'|'siteId'` — always false. Switched to the intended
`$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
to `string|false` to match `openssl_encrypt`; this lets callers'
`=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
`array_key_exists('from', [])` branch in the Msg91 provider (empty
array literal; branch was unreachable).
Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
`markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
Installer State, `$source` on MigrationsWorker, `$account2` on two
GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
`assertNotNull` assertions with `addToAssertionCount(1)` or
`assertNotEmpty` where the runtime type was already known.
348 lines
11 KiB
PHP
348 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Messaging;
|
|
|
|
use Appwrite\Messaging\Adapter\Realtime;
|
|
use Appwrite\Utopia\Database\Documents\User;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Utopia\Database\Helpers\ID;
|
|
use Utopia\Database\Helpers\Role;
|
|
use Utopia\Database\Validator\Authorization;
|
|
|
|
class MessagingChannelsTest extends TestCase
|
|
{
|
|
/**
|
|
* Configures how many Connections the Test should Mock.
|
|
*/
|
|
public $connectionsPerChannel = 10;
|
|
|
|
public ?Realtime $realtime = null;
|
|
public $connectionsCount = 0;
|
|
public $connectionsAuthenticated = 0;
|
|
public $connectionsGuest = 0;
|
|
public $connectionsTotal = 0;
|
|
public $allChannels = [
|
|
'files',
|
|
'files.1',
|
|
'collections',
|
|
'collections.1',
|
|
'collections.1.documents',
|
|
'documents',
|
|
'documents.1',
|
|
'executions',
|
|
'executions.1',
|
|
'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
|
|
{
|
|
/**
|
|
* Setup global Counts
|
|
*/
|
|
$this->connectionsAuthenticated = count($this->allChannels) * $this->connectionsPerChannel;
|
|
$this->connectionsGuest = count($this->allChannels) * $this->connectionsPerChannel;
|
|
$this->connectionsTotal = $this->connectionsAuthenticated + $this->connectionsGuest;
|
|
|
|
$this->realtime = new Realtime();
|
|
|
|
/**
|
|
* Add Authenticated Clients
|
|
*/
|
|
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
|
foreach ($this->allChannels as $index => $channel) {
|
|
$user = new User([
|
|
'$id' => ID::custom('user' . $this->connectionsCount),
|
|
'memberships' => [
|
|
[
|
|
'$id' => ID::custom('member' . $i),
|
|
'teamId' => ID::custom('team' . $i),
|
|
'confirm' => true,
|
|
'roles' => [
|
|
empty($index % 2)
|
|
? User::ROLE_ADMIN
|
|
: 'member',
|
|
]
|
|
]
|
|
]
|
|
]);
|
|
|
|
$roles = $user->getRoles($this->getAuthorization());
|
|
|
|
// Normalize channels to the format Realtime::subscribe expects (plain channel names)
|
|
$parsedChannels = array_keys(Realtime::convertChannels([0 => $channel], $user->getId()));
|
|
|
|
$this->realtime->subscribe(
|
|
'1',
|
|
$this->connectionsCount,
|
|
ID::unique(),
|
|
$roles,
|
|
$parsedChannels
|
|
);
|
|
|
|
$this->connectionsCount++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add Guest Clients
|
|
*/
|
|
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
|
foreach ($this->allChannels as $index => $channel) {
|
|
$user = new User([
|
|
'$id' => ''
|
|
]);
|
|
|
|
$roles = $user->getRoles($this->getAuthorization());
|
|
|
|
// Normalize channels to the format Realtime::subscribe expects (plain channel names)
|
|
$parsedChannels = array_keys(Realtime::convertChannels([0 => $channel], $user->getId()));
|
|
|
|
$this->realtime->subscribe(
|
|
'1',
|
|
$this->connectionsCount,
|
|
ID::unique(),
|
|
$roles,
|
|
$parsedChannels
|
|
);
|
|
|
|
$this->connectionsCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
public function tearDown(): void
|
|
{
|
|
$this->realtime = null;
|
|
$this->connectionsCount = 0;
|
|
}
|
|
|
|
public function testSubscriptions(): void
|
|
{
|
|
/**
|
|
* Check for 1 project.
|
|
*/
|
|
$this->assertCount(1, $this->realtime->subscriptions);
|
|
|
|
/**
|
|
* Check for correct amount of subscriptions:
|
|
* - XXX users (2 roles per user)
|
|
* - XXX teams
|
|
* - XXX team roles (2 roles per team)
|
|
* - XXX member roles (2 roles per team)
|
|
* - 1 guests
|
|
* - 1 users
|
|
* - 1 users unverified
|
|
*/
|
|
$userRoles = 2 * $this->connectionsAuthenticated;
|
|
$userGroupRoles = 2;
|
|
$teamRoles = 2 * $this->connectionsPerChannel;
|
|
$memberRoles = 2 * $this->connectionsPerChannel;
|
|
$guestRoles = 1;
|
|
$this->assertCount(($userRoles + $userGroupRoles + $teamRoles + $memberRoles + $guestRoles), $this->realtime->subscriptions['1']);
|
|
|
|
/**
|
|
* Check for connections
|
|
* - Authenticated
|
|
* - Guests
|
|
*/
|
|
$this->assertCount($this->connectionsTotal, $this->realtime->connections);
|
|
|
|
$this->realtime->unsubscribe(-1);
|
|
|
|
$this->assertCount($this->connectionsTotal, $this->realtime->connections);
|
|
$this->assertCount(($userRoles + $userGroupRoles + $teamRoles + $memberRoles + $guestRoles), $this->realtime->subscriptions['1']);
|
|
|
|
for ($i = 0; $i < $this->connectionsCount; $i++) {
|
|
$this->realtime->unsubscribe($i);
|
|
|
|
$this->assertCount(($this->connectionsCount - $i - 1), $this->realtime->connections);
|
|
}
|
|
|
|
$this->assertEmpty($this->realtime->connections);
|
|
$this->assertEmpty($this->realtime->subscriptions);
|
|
}
|
|
|
|
/**
|
|
* Tests Wildcard ("any") Permissions on every channel.
|
|
*/
|
|
public function testWildcardPermission(): void
|
|
{
|
|
foreach ($this->allChannels as $index => $channel) {
|
|
$event = [
|
|
'project' => '1',
|
|
'roles' => [Role::any()->toString()],
|
|
'data' => [
|
|
'channels' => [
|
|
0 => $channel,
|
|
]
|
|
]
|
|
];
|
|
|
|
$receivers = $this->realtime->getSubscribers($event);
|
|
|
|
/**
|
|
* Every Client subscribed to the Wildcard should receive this event.
|
|
*/
|
|
$this->assertCount($this->connectionsTotal / count($this->allChannels), $receivers, $channel);
|
|
|
|
foreach ($receivers as $receiverId => $queryKeys) {
|
|
/**
|
|
* Making sure the right clients receive the event.
|
|
*/
|
|
$this->assertStringEndsWith($index, $receiverId);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testRolePermissions(): void
|
|
{
|
|
$roles = [
|
|
Role::guests()->toString(),
|
|
Role::users()->toString()
|
|
];
|
|
foreach ($this->allChannels as $index => $channel) {
|
|
foreach ($roles as $role) {
|
|
$permissions = [$role];
|
|
|
|
$event = [
|
|
'project' => '1',
|
|
'roles' => $permissions,
|
|
'data' => [
|
|
'channels' => [
|
|
0 => $channel,
|
|
]
|
|
]
|
|
];
|
|
|
|
$receivers = $this->realtime->getSubscribers($event);
|
|
|
|
/**
|
|
* Every Role subscribed to a Channel should receive this event.
|
|
*/
|
|
$this->assertCount($this->connectionsPerChannel, $receivers, $channel);
|
|
|
|
foreach ($receivers as $receiverId => $queryKeys) {
|
|
/**
|
|
* Making sure the right clients receive the event.
|
|
*/
|
|
$this->assertStringEndsWith($index, $receiverId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testUserPermissions(): void
|
|
{
|
|
foreach ($this->allChannels as $index => $channel) {
|
|
$permissions = [];
|
|
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
|
$permissions[] = Role::user(ID::custom('user' . (!empty($i) ? $i : '') . $index))->toString();
|
|
}
|
|
$event = [
|
|
'project' => '1',
|
|
'roles' => $permissions,
|
|
'data' => [
|
|
'channels' => [
|
|
0 => $channel,
|
|
]
|
|
]
|
|
];
|
|
|
|
$receivers = array_keys($this->realtime->getSubscribers($event));
|
|
|
|
/**
|
|
* Every Client subscribed to a Channel should receive this event.
|
|
*/
|
|
$this->assertCount($this->connectionsAuthenticated / count($this->allChannels), $receivers, $channel);
|
|
|
|
foreach ($receivers as $receiver) {
|
|
/**
|
|
* Making sure the right clients receive the event.
|
|
*/
|
|
$this->assertStringEndsWith($index, $receiver);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testTeamPermissions(): void
|
|
{
|
|
foreach ($this->allChannels as $index => $channel) {
|
|
$permissions = [];
|
|
|
|
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
|
$permissions[] = Role::team(ID::custom('team' . $i))->toString();
|
|
$permissions[] = Role::member(ID::custom('member' . $i))->toString();
|
|
}
|
|
$event = [
|
|
'project' => '1',
|
|
'roles' => $permissions,
|
|
'data' => [
|
|
'channels' => [
|
|
0 => $channel,
|
|
]
|
|
]
|
|
];
|
|
|
|
$receivers = array_keys($this->realtime->getSubscribers($event));
|
|
|
|
/**
|
|
* Every Team Member should receive this event.
|
|
*/
|
|
$this->assertCount($this->connectionsAuthenticated / count($this->allChannels), $receivers, $channel);
|
|
|
|
foreach ($receivers as $receiver) {
|
|
/**
|
|
* Making sure the right clients receive the event.
|
|
*/
|
|
$this->assertStringEndsWith($index, $receiver);
|
|
}
|
|
|
|
$role = empty($index % 2)
|
|
? User::ROLE_ADMIN
|
|
: 'member';
|
|
|
|
$permissions = [
|
|
Role::team(ID::custom('team' . $index), $role)->toString(),
|
|
Role::member(ID::custom('member' . $index))->toString()
|
|
];
|
|
|
|
$event = [
|
|
'project' => '1',
|
|
'roles' => $permissions,
|
|
'data' => [
|
|
'channels' => [
|
|
0 => $channel,
|
|
]
|
|
]
|
|
];
|
|
|
|
$receivers = array_keys($this->realtime->getSubscribers($event));
|
|
|
|
/**
|
|
* Only 1 Team Member of a role should have access to a specific channel.
|
|
*/
|
|
$this->assertCount(1, $receivers, $channel);
|
|
|
|
foreach ($receivers as $receiver) {
|
|
/**
|
|
* Making sure the right clients receive the event.
|
|
*/
|
|
$this->assertStringEndsWith($index, $receiver);
|
|
}
|
|
}
|
|
}
|
|
}
|