Files
appwrite/tests/unit/Messaging/MessagingChannelsTest.php
T
Chirag Aggarwal d2230f8fe7 chore: bump PHPStan to level 4 and fix all new errors
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.
2026-04-19 17:31:20 +05:30

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);
}
}
}
}