diff --git a/src/Generated/ChatTrait.php b/src/Generated/ChatTrait.php index bb58999..0fefa1f 100644 --- a/src/Generated/ChatTrait.php +++ b/src/Generated/ChatTrait.php @@ -1321,6 +1321,68 @@ public function queryReminders(GeneratedModels\QueryRemindersRequest $requestDat // Use the provided request data array directly return StreamResponse::fromJson($this->makeRequest('POST', $path, $queryParams, $requestData), GeneratedModels\QueryRemindersResponse::class); } + /** + * Returns all retention policies configured for the app. Server-side only. + * + * @return StreamResponse + * @throws StreamException + */ + public function getRetentionPolicy(): StreamResponse { + $path = '/api/v2/chat/retention_policy'; + + $queryParams = []; + $requestData = null; + return StreamResponse::fromJson($this->makeRequest('GET', $path, $queryParams, $requestData), GeneratedModels\GetRetentionPolicyResponse::class); + } + /** + * Creates or updates a retention policy for the app. Server-side only. + * + * @param GeneratedModels\SetRetentionPolicyRequest $requestData + * @return StreamResponse + * @throws StreamException + */ + public function setRetentionPolicy(GeneratedModels\SetRetentionPolicyRequest $requestData): StreamResponse { + $path = '/api/v2/chat/retention_policy'; + + $queryParams = []; + // Use the provided request data array directly + return StreamResponse::fromJson($this->makeRequest('POST', $path, $queryParams, $requestData), GeneratedModels\SetRetentionPolicyResponse::class); + } + /** + * Removes a retention policy for the app. Server-side only. + * + * @param GeneratedModels\DeleteRetentionPolicyRequest $requestData + * @return StreamResponse + * @throws StreamException + */ + public function deleteRetentionPolicy(GeneratedModels\DeleteRetentionPolicyRequest $requestData): StreamResponse { + $path = '/api/v2/chat/retention_policy/delete'; + + $queryParams = []; + // Use the provided request data array directly + return StreamResponse::fromJson($this->makeRequest('POST', $path, $queryParams, $requestData), GeneratedModels\DeleteRetentionPolicyResponse::class); + } + /** + * Returns paginated retention cleanup run history for the app. Server-side only. + * + * @param int $limit + * @param int $offset + * @return StreamResponse + * @throws StreamException + */ + public function getRetentionPolicyRuns(int $limit, int $offset): StreamResponse { + $path = '/api/v2/chat/retention_policy/runs'; + + $queryParams = []; + if ($limit !== null) { + $queryParams['limit'] = $limit; + } + if ($offset !== null) { + $queryParams['offset'] = $offset; + } + $requestData = null; + return StreamResponse::fromJson($this->makeRequest('GET', $path, $queryParams, $requestData), GeneratedModels\GetRetentionPolicyRunsResponse::class); + } /** * Search messages across channels * diff --git a/src/Generated/VideoTrait.php b/src/Generated/VideoTrait.php index 5a18757..54304ff 100644 --- a/src/Generated/VideoTrait.php +++ b/src/Generated/VideoTrait.php @@ -1115,6 +1115,20 @@ public function getEdges(): StreamResponse { $requestData = null; return StreamResponse::fromJson($this->makeRequest('GET', $path, $queryParams, $requestData), GeneratedModels\GetEdgesResponse::class); } + /** + * Determine authentication requirements for an inbound SIP call before sending a digest challenge + * + * @param GeneratedModels\ResolveSipAuthRequest $requestData + * @return StreamResponse + * @throws StreamException + */ + public function resolveSipAuth(GeneratedModels\ResolveSipAuthRequest $requestData): StreamResponse { + $path = '/api/v2/video/sip/auth'; + + $queryParams = []; + // Use the provided request data array directly + return StreamResponse::fromJson($this->makeRequest('POST', $path, $queryParams, $requestData), GeneratedModels\ResolveSipAuthResponse::class); + } /** * List all SIP Inbound Routing Rules for the application * diff --git a/src/GeneratedModels/AIImageLabelDefinition.php b/src/GeneratedModels/AIImageLabelDefinition.php new file mode 100644 index 0000000..e7d861c --- /dev/null +++ b/src/GeneratedModels/AIImageLabelDefinition.php @@ -0,0 +1,18 @@ +|null */ + #[ArrayOf(MemberResponse::class)] + public ?array $members = null, // The list of members in the call ) { } diff --git a/src/GeneratedModels/ChannelConfig.php b/src/GeneratedModels/ChannelConfig.php index fc89708..a283115 100644 --- a/src/GeneratedModels/ChannelConfig.php +++ b/src/GeneratedModels/ChannelConfig.php @@ -40,6 +40,7 @@ public function __construct( public ?string $partitionTtl = null, public ?bool $skipLastMsgUpdateForSystemMsgs = null, public ?string $pushLevel = null, + public ?ChatPreferences $chatPreferences = null, public ?\DateTime $createdAt = null, public ?\DateTime $updatedAt = null, public ?array $commands = null, // List of commands that channel supports diff --git a/src/GeneratedModels/ChannelConfigWithInfo.php b/src/GeneratedModels/ChannelConfigWithInfo.php index da40d64..d27c360 100644 --- a/src/GeneratedModels/ChannelConfigWithInfo.php +++ b/src/GeneratedModels/ChannelConfigWithInfo.php @@ -42,6 +42,7 @@ public function __construct( public ?string $partitionTtl = null, public ?bool $skipLastMsgUpdateForSystemMsgs = null, public ?string $pushLevel = null, + public ?ChatPreferences $chatPreferences = null, /** @var array|null */ #[ArrayOf(Command::class)] public ?array $commands = null, diff --git a/src/GeneratedModels/ChannelPushPreferencesResponse.php b/src/GeneratedModels/ChannelPushPreferencesResponse.php index 6bbc4f4..12bedea 100644 --- a/src/GeneratedModels/ChannelPushPreferencesResponse.php +++ b/src/GeneratedModels/ChannelPushPreferencesResponse.php @@ -7,6 +7,7 @@ class ChannelPushPreferencesResponse extends BaseModel { public function __construct( public ?string $chatLevel = null, + public ?ChatPreferencesResponse $chatPreferences = null, public ?\DateTime $disabledUntil = null, ) { } diff --git a/src/GeneratedModels/ChannelTypeConfig.php b/src/GeneratedModels/ChannelTypeConfig.php index 8a54130..275da87 100644 --- a/src/GeneratedModels/ChannelTypeConfig.php +++ b/src/GeneratedModels/ChannelTypeConfig.php @@ -46,6 +46,7 @@ public function __construct( public ?string $partitionTtl = null, public ?bool $skipLastMsgUpdateForSystemMsgs = null, public ?string $pushLevel = null, + public ?ChatPreferences $chatPreferences = null, /** @var array|null */ #[ArrayOf(Command::class)] public ?array $commands = null, diff --git a/src/GeneratedModels/ChatPreferences.php b/src/GeneratedModels/ChatPreferences.php new file mode 100644 index 0000000..9d356fa --- /dev/null +++ b/src/GeneratedModels/ChatPreferences.php @@ -0,0 +1,22 @@ +|null */ + #[ArrayOf(AIImageLabelDefinition::class)] + public ?array $aiImageLabelDefinitions = null, // Configurable image moderation label definitions for dashboard rendering ) { } diff --git a/src/GeneratedModels/CreateChannelTypeRequest.php b/src/GeneratedModels/CreateChannelTypeRequest.php index 5982dae..cf4b3e6 100644 --- a/src/GeneratedModels/CreateChannelTypeRequest.php +++ b/src/GeneratedModels/CreateChannelTypeRequest.php @@ -42,6 +42,7 @@ public function __construct( public ?string $partitionTtl = null, // Partition TTL public ?bool $skipLastMsgUpdateForSystemMsgs = null, public ?string $pushLevel = null, // Default push notification level for the channel type. One of: all, all_mentions, mentions, direct_mentions, none + public ?ChatPreferences $chatPreferences = null, ) { } diff --git a/src/GeneratedModels/CreateChannelTypeResponse.php b/src/GeneratedModels/CreateChannelTypeResponse.php index eca0762..44954b1 100644 --- a/src/GeneratedModels/CreateChannelTypeResponse.php +++ b/src/GeneratedModels/CreateChannelTypeResponse.php @@ -40,6 +40,7 @@ public function __construct( public ?string $partitionTtl = null, public ?bool $skipLastMsgUpdateForSystemMsgs = null, public ?string $pushLevel = null, + public ?ChatPreferences $chatPreferences = null, public ?\DateTime $createdAt = null, public ?\DateTime $updatedAt = null, public ?array $commands = null, diff --git a/src/GeneratedModels/CreateSIPTrunkRequest.php b/src/GeneratedModels/CreateSIPTrunkRequest.php index 47fe3fb..d99c6ca 100644 --- a/src/GeneratedModels/CreateSIPTrunkRequest.php +++ b/src/GeneratedModels/CreateSIPTrunkRequest.php @@ -11,6 +11,8 @@ class CreateSIPTrunkRequest extends BaseModel public function __construct( public ?string $name = null, // Name of the SIP trunk public ?array $numbers = null, // Phone numbers associated with this SIP trunk + public ?string $password = null, // Optional password for SIP trunk authentication + public ?array $allowedIps = null, // Optional list of allowed IPv4/IPv6 addresses or CIDR blocks ) { } diff --git a/src/GeneratedModels/DeleteRetentionPolicyRequest.php b/src/GeneratedModels/DeleteRetentionPolicyRequest.php new file mode 100644 index 0000000..4ba34c8 --- /dev/null +++ b/src/GeneratedModels/DeleteRetentionPolicyRequest.php @@ -0,0 +1,15 @@ +|null */ #[ArrayOf(Command::class)] public ?array $commands = null, diff --git a/src/GeneratedModels/GetRetentionPolicyResponse.php b/src/GeneratedModels/GetRetentionPolicyResponse.php new file mode 100644 index 0000000..bdfaf01 --- /dev/null +++ b/src/GeneratedModels/GetRetentionPolicyResponse.php @@ -0,0 +1,21 @@ +|null */ + #[ArrayOf(RetentionPolicy::class)] + public ?array $policies = null, + public ?string $duration = null, // Duration of the request in milliseconds + ) { + } + + // BaseModel automatically handles jsonSerialize(), toArray(), and fromJson() using constructor types! + // Use #[JsonKey('user_id')] to override field names if needed. +} diff --git a/src/GeneratedModels/GetRetentionPolicyRunsResponse.php b/src/GeneratedModels/GetRetentionPolicyRunsResponse.php new file mode 100644 index 0000000..52399ff --- /dev/null +++ b/src/GeneratedModels/GetRetentionPolicyRunsResponse.php @@ -0,0 +1,21 @@ +|null */ + #[ArrayOf(RetentionCleanupRun::class)] + public ?array $runs = null, + public ?string $duration = null, // Duration of the request in milliseconds + ) { + } + + // BaseModel automatically handles jsonSerialize(), toArray(), and fromJson() using constructor types! + // Use #[JsonKey('user_id')] to override field names if needed. +} diff --git a/src/GeneratedModels/PolicyConfig.php b/src/GeneratedModels/PolicyConfig.php new file mode 100644 index 0000000..9cac13e --- /dev/null +++ b/src/GeneratedModels/PolicyConfig.php @@ -0,0 +1,15 @@ +|null */ + #[ArrayOf(AIImageLabelDefinition::class)] + public ?array $aiImageLabelDefinitions = null, // AI image label definitions with metadata for dashboard rendering + public ?array $aiImageSubclassifications = null, // Stream L1 to leaf-level label name mapping for AI image rules public ?string $next = null, public ?string $prev = null, public ?string $duration = null, diff --git a/src/GeneratedModels/ResolveSipAuthRequest.php b/src/GeneratedModels/ResolveSipAuthRequest.php new file mode 100644 index 0000000..be0f67d --- /dev/null +++ b/src/GeneratedModels/ResolveSipAuthRequest.php @@ -0,0 +1,21 @@ +getData(); self::assertNotEmpty($data->channels); - self::assertEquals($channelID, $data->channels[0]->channel->id); - self::assertEquals('messaging', $data->channels[0]->channel->type); + self::assertSame($channelID, $data->channels[0]->channel->id); + self::assertSame('messaging', $data->channels[0]->channel->type); } /** @@ -97,7 +92,7 @@ public function testCreateDistinctChannel(): void )); $this->assertResponseSuccess($resp2, 'create distinct channel (second call)'); - self::assertEquals($resp1->getData()->channel->cid, $resp2->getData()->channel->cid); + self::assertSame($resp1->getData()->channel->cid, $resp2->getData()->channel->cid); // Track for cleanup $this->createdChannels[] = ['type' => 'messaging', 'id' => $resp1->getData()->channel->id]; @@ -116,7 +111,7 @@ public function testQueryChannels(): void $this->assertResponseSuccess($resp, 'query channels'); self::assertNotEmpty($resp->getData()->channels); - self::assertEquals($channelID, $resp->getData()->channels[0]->channel->id); + self::assertSame($channelID, $resp->getData()->channels[0]->channel->id); } /** @@ -138,7 +133,7 @@ public function testUpdateChannel(): void $this->assertResponseSuccess($resp, 'update channel'); self::assertNotNull($resp->getData()->channel); - self::assertEquals('blue', $resp->getData()->channel->custom->color ?? null); + self::assertSame('blue', $resp->getData()->channel->custom->color ?? null); } /** @@ -157,7 +152,7 @@ public function testPartialUpdateChannel(): void )); $this->assertResponseSuccess($resp, 'partial update channel (set)'); self::assertNotNull($resp->getData()->channel); - self::assertEquals('red', $resp->getData()->channel->custom->color ?? null); + self::assertSame('red', $resp->getData()->channel->custom->color ?? null); // Unset fields $resp = $this->updateChannelPartial($type, $channelID, new GeneratedModels\UpdateChannelPartialRequest( @@ -205,7 +200,7 @@ public function testHardDeleteChannels(): void $cid2 = "messaging:{$channelID2}"; // Remove from cleanup tracking since we're hard-deleting them here - $this->createdChannels = array_filter($this->createdChannels, function ($ch) use ($channelID1, $channelID2) { + $this->createdChannels = array_filter($this->createdChannels, static function ($ch) use ($channelID1, $channelID2) { return $ch['id'] !== $channelID1 && $ch['id'] !== $channelID2; }); @@ -217,7 +212,7 @@ public function testHardDeleteChannels(): void self::assertNotEmpty($resp->getData()->taskID); $taskResult = $this->waitForTask($resp->getData()->taskID); - self::assertEquals('completed', $taskResult->status); + self::assertSame('completed', $taskResult->status); } /** @@ -253,7 +248,7 @@ public function testAddRemoveMembers(): void // Verify member removed $stateResp = $this->getOrCreateChannel($type, $channelID, new GeneratedModels\ChannelGetOrCreateRequest()); $this->assertResponseSuccess($stateResp, 'get channel after remove member'); - $memberIDs = array_map(fn($m) => $m->userID, $stateResp->getData()->members ?? []); + $memberIDs = array_map(static fn ($m) => $m->userID, $stateResp->getData()->members ?? []); self::assertNotContains($this->memberID3, $memberIDs, 'memberID3 should have been removed'); } @@ -437,7 +432,7 @@ public function testMuteUnmuteChannel(): void )); $this->assertResponseSuccess($qResp, 'query muted channels'); self::assertCount(1, $qResp->getData()->channels, 'Should find exactly 1 muted channel'); - self::assertEquals($cid, $qResp->getData()->channels[0]->channel->cid); + self::assertSame($cid, $qResp->getData()->channels[0]->channel->cid); // Unmute $resp = $this->unmuteChannel(new GeneratedModels\UnmuteChannelRequest( @@ -474,7 +469,7 @@ public function testMemberPartialUpdate(): void )); $this->assertResponseSuccess($resp, 'member partial update (set)'); self::assertNotNull($resp->getData()->channelMember); - self::assertEquals('moderator', $resp->getData()->channelMember->custom->role_label ?? null); + self::assertSame('moderator', $resp->getData()->channelMember->custom->role_label ?? null); // Unset a custom field $resp = $this->updateMemberPartial($type, $channelID, $this->memberID1, new GeneratedModels\UpdateMemberPartialRequest( @@ -518,7 +513,7 @@ public function testAssignRoles(): void )); $this->assertResponseSuccess($qResp, 'query members after assign roles'); self::assertNotEmpty($qResp->getData()->members); - self::assertEquals('channel_moderator', $qResp->getData()->members[0]->channelRole); + self::assertSame('channel_moderator', $qResp->getData()->members[0]->channelRole); } /** @@ -545,7 +540,7 @@ public function testAddDemoteModerators(): void )); $this->assertResponseSuccess($qResp, 'query members after add moderator'); self::assertNotEmpty($qResp->getData()->members); - self::assertEquals('channel_moderator', $qResp->getData()->members[0]->channelRole); + self::assertSame('channel_moderator', $qResp->getData()->members[0]->channelRole); // Demote moderator back to member $resp = $this->updateChannel($type, $channelID, new GeneratedModels\UpdateChannelRequest( @@ -561,7 +556,7 @@ public function testAddDemoteModerators(): void )); $this->assertResponseSuccess($qResp, 'query members after demote moderator'); self::assertNotEmpty($qResp->getData()->members); - self::assertEquals('channel_member', $qResp->getData()->members[0]->channelRole); + self::assertSame('channel_member', $qResp->getData()->members[0]->channelRole); } /** @@ -647,7 +642,7 @@ public function testPinUnpinChannel(): void )); $this->assertResponseSuccess($qResp, 'query pinned channels'); self::assertCount(1, $qResp->getData()->channels, 'Should find 1 pinned channel'); - self::assertEquals("messaging:{$channelID}", $qResp->getData()->channels[0]->channel->cid); + self::assertSame("messaging:{$channelID}", $qResp->getData()->channels[0]->channel->cid); // Unpin $resp = $this->updateMemberPartial($type, $channelID, $this->memberID1, new GeneratedModels\UpdateMemberPartialRequest( @@ -746,8 +741,8 @@ public function testAddMembersWithRoles(): void $roleMap[$m->userID] = $m->channelRole; } } - self::assertEquals('channel_moderator', $roleMap[$modUserID] ?? null, 'First user should be channel_moderator'); - self::assertEquals('channel_member', $roleMap[$memberUserID] ?? null, 'Second user should be channel_member'); + self::assertSame('channel_moderator', $roleMap[$modUserID] ?? null, 'First user should be channel_moderator'); + self::assertSame('channel_member', $roleMap[$memberUserID] ?? null, 'Second user should be channel_member'); } /** @@ -997,7 +992,8 @@ public function testUploadAndDeleteImage(): void $tmpFile = tempnam(sys_get_temp_dir(), 'chat-test-') . '.png'; // Minimal valid PNG: 1x1 white pixel $pngData = base64_decode( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==' + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', + true ); file_put_contents($tmpFile, $pngData); @@ -1034,4 +1030,9 @@ public function testUploadAndDeleteImage(): void @unlink($tmpFile); } } + + protected static function sharedUserCount(): int + { + return 4; + } } diff --git a/tests/Integration/ChatMessageIntegrationTest.php b/tests/Integration/ChatMessageIntegrationTest.php index 26c6f10..bbce851 100644 --- a/tests/Integration/ChatMessageIntegrationTest.php +++ b/tests/Integration/ChatMessageIntegrationTest.php @@ -17,11 +17,6 @@ class ChatMessageIntegrationTest extends ChatTestCase private string $userID; private string $userID2; - protected static function sharedUserCount(): int - { - return 2; - } - protected function setUp(): void { parent::setUp(); @@ -34,7 +29,10 @@ protected function setUp(): void // Send / Get / Update / Delete // ========================================================================= - public function testSendAndGetMessage(): void + /** + * @test + */ + public function sendAndGetMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -47,11 +45,14 @@ public function testSendAndGetMessage(): void $data = $resp->getData(); self::assertNotNull($data->message); - self::assertEquals($msgID, $data->message->id); - self::assertEquals($msgText, $data->message->text); + self::assertSame($msgID, $data->message->id); + self::assertSame($msgText, $data->message->text); } - public function testGetManyMessages(): void + /** + * @test + */ + public function getsManyMessages(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -66,7 +67,10 @@ public function testGetManyMessages(): void self::assertCount(3, $data->messages); } - public function testUpdateMessage(): void + /** + * @test + */ + public function updatesMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); $msgID = $this->sendTestMessage($type, $channelID, $this->userID, 'Original text'); @@ -79,10 +83,13 @@ public function testUpdateMessage(): void ), )); $this->assertResponseSuccess($resp, 'update message'); - self::assertEquals($updatedText, $resp->getData()->message->text); + self::assertSame($updatedText, $resp->getData()->message->text); } - public function testPartialUpdateMessage(): void + /** + * @test + */ + public function partialUpdateMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); $msgID = $this->sendTestMessage($type, $channelID, $this->userID, 'Partial update test'); @@ -107,7 +114,10 @@ public function testPartialUpdateMessage(): void self::assertNotNull($resp->getData()->message); } - public function testDeleteMessage(): void + /** + * @test + */ + public function deleteMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); $msgID = $this->sendTestMessage($type, $channelID, $this->userID, 'Message to delete'); @@ -115,24 +125,30 @@ public function testDeleteMessage(): void // Soft delete $resp = $this->deleteMessageApi($msgID); $this->assertResponseSuccess($resp, 'delete message'); - self::assertEquals('deleted', $resp->getData()->message->type); + self::assertSame('deleted', $resp->getData()->message->type); } - public function testHardDeleteMessage(): void + /** + * @test + */ + public function hardDeleteMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); $msgID = $this->sendTestMessage($type, $channelID, $this->userID, 'Message to hard delete'); $resp = $this->deleteMessageApi($msgID, hard: true); $this->assertResponseSuccess($resp, 'hard delete message'); - self::assertEquals('deleted', $resp->getData()->message->type); + self::assertSame('deleted', $resp->getData()->message->type); } // ========================================================================= // Pin / Unpin // ========================================================================= - public function testPinUnpinMessage(): void + /** + * @test + */ + public function pinUnpinMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -157,7 +173,10 @@ public function testPinUnpinMessage(): void self::assertFalse($resp->getData()->message->pinned); } - public function testPinExpiration(): void + /** + * @test + */ + public function pinExpiration(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID, $this->userID2]); @@ -189,7 +208,10 @@ public function testPinExpiration(): void // Translate // ========================================================================= - public function testTranslateMessage(): void + /** + * @test + */ + public function translatesMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); $msgID = $this->sendTestMessage($type, $channelID, $this->userID, 'Hello, how are you?'); @@ -206,7 +228,10 @@ public function testTranslateMessage(): void // Threads // ========================================================================= - public function testThreadReply(): void + /** + * @test + */ + public function threadReply(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID, $this->userID2]); @@ -234,7 +259,10 @@ public function testThreadReply(): void // Search // ========================================================================= - public function testSearchMessages(): void + /** + * @test + */ + public function searchesMessages(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -252,7 +280,10 @@ public function testSearchMessages(): void self::assertNotEmpty($resp->getData()->results, 'Search should return at least one result'); } - public function testSearchWithMessageFilters(): void + /** + * @test + */ + public function searchWithMessageFilters(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -272,7 +303,10 @@ public function testSearchWithMessageFilters(): void self::assertGreaterThanOrEqual(2, count($resp->getData()->results), 'Should find at least 2 messages with MessageFilterConditions'); } - public function testSearchQueryAndMessageFiltersError(): void + /** + * @test + */ + public function searchQueryAndMessageFiltersError(): void { // Using both query and messageFilterConditions together should error $this->expectException(\Exception::class); @@ -283,8 +317,10 @@ public function testSearchQueryAndMessageFiltersError(): void )); } - - public function testSearchOffsetAndNextError(): void + /** + * @test + */ + public function searchOffsetAndNextError(): void { // Using offset with next should error $this->expectException(\Exception::class); @@ -300,7 +336,10 @@ public function testSearchOffsetAndNextError(): void // Special Message Types // ========================================================================= - public function testSilentMessage(): void + /** + * @test + */ + public function silentMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -315,7 +354,10 @@ public function testSilentMessage(): void self::assertTrue($resp->getData()->message->silent); } - public function testSystemMessage(): void + /** + * @test + */ + public function systemMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -327,14 +369,17 @@ public function testSystemMessage(): void ), )); $this->assertResponseSuccess($resp, 'send system message'); - self::assertEquals('system', $resp->getData()->message->type); + self::assertSame('system', $resp->getData()->message->type); } // ========================================================================= // Pending Messages // ========================================================================= - public function testPendingMessage(): void + /** + * @test + */ + public function pendingMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -350,8 +395,9 @@ public function testPendingMessage(): void )); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'pending messages not enabled') || str_contains($e->getMessage(), 'feature flag')) { - $this->markTestSkipped('Pending messages feature not enabled for this app'); + self::markTestSkipped('Pending messages feature not enabled for this app'); } + throw $e; } $this->assertResponseSuccess($sendResp, 'send pending message'); @@ -362,10 +408,13 @@ public function testPendingMessage(): void $commitResp = $this->commitMessage($msgID); $this->assertResponseSuccess($commitResp, 'commit pending message'); self::assertNotNull($commitResp->getData()->message); - self::assertEquals($msgID, $commitResp->getData()->message->id); + self::assertSame($msgID, $commitResp->getData()->message->id); } - public function testPendingFalse(): void + /** + * @test + */ + public function pendingFalse(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -382,14 +431,17 @@ public function testPendingFalse(): void // Get the message to verify it's immediately available $getResp = $this->getMessage($sendResp->getData()->message->id); $this->assertResponseSuccess($getResp, 'get non-pending message'); - self::assertEquals('Non-pending message', $getResp->getData()->message->text); + self::assertSame('Non-pending message', $getResp->getData()->message->text); } // ========================================================================= // Message History // ========================================================================= - public function testQueryMessageHistory(): void + /** + * @test + */ + public function queriesMessageHistory(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID, $this->userID2]); @@ -429,8 +481,9 @@ public function testQueryMessageHistory(): void )); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'feature flag') || str_contains($e->getMessage(), 'not enabled')) { - $this->markTestSkipped('QueryMessageHistory feature not enabled for this app'); + self::markTestSkipped('QueryMessageHistory feature not enabled for this app'); } + throw $e; } $this->assertResponseSuccess($histResp, 'query message history'); @@ -438,17 +491,20 @@ public function testQueryMessageHistory(): void // Verify history entries reference the correct message foreach ($histResp->getData()->messageHistory as $entry) { - self::assertEquals($msgID, $entry->messageID); + self::assertSame($msgID, $entry->messageID); } // Verify text values in history (descending order by default) - self::assertEquals('updated text', $histResp->getData()->messageHistory[0]->text); - self::assertEquals($this->userID, $histResp->getData()->messageHistory[0]->messageUpdatedByID); - self::assertEquals('initial text', $histResp->getData()->messageHistory[1]->text); - self::assertEquals($this->userID, $histResp->getData()->messageHistory[1]->messageUpdatedByID); + self::assertSame('updated text', $histResp->getData()->messageHistory[0]->text); + self::assertSame($this->userID, $histResp->getData()->messageHistory[0]->messageUpdatedByID); + self::assertSame('initial text', $histResp->getData()->messageHistory[1]->text); + self::assertSame($this->userID, $histResp->getData()->messageHistory[1]->messageUpdatedByID); } - public function testQueryMessageHistorySort(): void + /** + * @test + */ + public function queryMessageHistorySort(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID, $this->userID2]); @@ -483,23 +539,27 @@ public function testQueryMessageHistorySort(): void )); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'feature flag') || str_contains($e->getMessage(), 'not enabled')) { - $this->markTestSkipped('QueryMessageHistory feature not enabled for this app'); + self::markTestSkipped('QueryMessageHistory feature not enabled for this app'); } + throw $e; } $this->assertResponseSuccess($histResp, 'query message history with sort'); self::assertGreaterThanOrEqual(2, count($histResp->getData()->messageHistory)); // Ascending: oldest first - self::assertEquals('sort initial', $histResp->getData()->messageHistory[0]->text); - self::assertEquals($this->userID, $histResp->getData()->messageHistory[0]->messageUpdatedByID); + self::assertSame('sort initial', $histResp->getData()->messageHistory[0]->text); + self::assertSame($this->userID, $histResp->getData()->messageHistory[0]->messageUpdatedByID); } // ========================================================================= // URL Enrichment // ========================================================================= - public function testSkipEnrichUrl(): void + /** + * @test + */ + public function skipEnrichUrl(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -525,7 +585,10 @@ public function testSkipEnrichUrl(): void // Channel Hidden / Visibility // ========================================================================= - public function testKeepChannelHidden(): void + /** + * @test + */ + public function keepChannelHidden(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); @@ -558,7 +621,10 @@ public function testKeepChannelHidden(): void // Delete / Undelete Variants // ========================================================================= - public function testUndeleteMessage(): void + /** + * @test + */ + public function undeletesMessage(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); $msgID = $this->sendTestMessage($type, $channelID, $this->userID, 'Message to undelete'); @@ -570,7 +636,7 @@ public function testUndeleteMessage(): void // Verify it's deleted $getResp = $this->getMessage($msgID); $this->assertResponseSuccess($getResp, 'get deleted message'); - self::assertEquals('deleted', $getResp->getData()->message->type); + self::assertSame('deleted', $getResp->getData()->message->type); // Undelete the message try { @@ -579,17 +645,21 @@ public function testUndeleteMessage(): void )); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'undeleted_by') || str_contains($e->getMessage(), 'required field')) { - $this->markTestSkipped('UndeleteMessage requires field not yet in generated request struct'); + self::markTestSkipped('UndeleteMessage requires field not yet in generated request struct'); } + throw $e; } $this->assertResponseSuccess($undelResp, 'undelete message'); self::assertNotNull($undelResp->getData()->message); - self::assertNotEquals('deleted', $undelResp->getData()->message->type); - self::assertEquals('Message to undelete', $undelResp->getData()->message->text); + self::assertNotSame('deleted', $undelResp->getData()->message->type); + self::assertSame('Message to undelete', $undelResp->getData()->message->text); } - public function testRestrictedVisibility(): void + /** + * @test + */ + public function restrictedVisibility(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID, $this->userID2]); @@ -604,15 +674,19 @@ public function testRestrictedVisibility(): void )); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'private messaging is not allowed') || str_contains($e->getMessage(), 'not enabled')) { - $this->markTestSkipped('RestrictedVisibility (private messaging) is not enabled for this app'); + self::markTestSkipped('RestrictedVisibility (private messaging) is not enabled for this app'); } + throw $e; } $this->assertResponseSuccess($sendResp, 'send restricted visibility message'); - self::assertEquals([$this->userID], $sendResp->getData()->message->restrictedVisibility); + self::assertSame([$this->userID], $sendResp->getData()->message->restrictedVisibility); } - public function testDeleteMessageForMe(): void + /** + * @test + */ + public function deleteMessageForMe(): void { [$type, $channelID] = $this->createTestChannelWithMembers($this->userID, [$this->userID]); $msgID = $this->sendTestMessage($type, $channelID, $this->userID, 'test message to delete for me'); @@ -626,7 +700,10 @@ public function testDeleteMessageForMe(): void // Channel Role in Member // ========================================================================= - public function testChannelRoleInMember(): void + /** + * @test + */ + public function channelRoleInMember(): void { $roleUserIDs = $this->createTestUsers(2); $memberUserID = $roleUserIDs[0]; @@ -656,7 +733,7 @@ public function testChannelRoleInMember(): void )); $this->assertResponseSuccess($respMember, 'send from channel_member'); self::assertNotNull($respMember->getData()->message->member, 'Member should be present in message response'); - self::assertEquals('channel_member', $respMember->getData()->message->member->channelRole); + self::assertSame('channel_member', $respMember->getData()->message->member->channelRole); // Send message from channel_moderator $respMod = $this->sendMessage($channelType, $channelID, new GeneratedModels\SendMessageRequest( @@ -667,6 +744,11 @@ public function testChannelRoleInMember(): void )); $this->assertResponseSuccess($respMod, 'send from channel_moderator'); self::assertNotNull($respMod->getData()->message->member, 'Member should be present in message response'); - self::assertEquals('channel_moderator', $respMod->getData()->message->member->channelRole); + self::assertSame('channel_moderator', $respMod->getData()->message->member->channelRole); + } + + protected static function sharedUserCount(): int + { + return 2; } } diff --git a/tests/Integration/ChatMiscIntegrationTest.php b/tests/Integration/ChatMiscIntegrationTest.php index 68c3c4f..26e3853 100644 --- a/tests/Integration/ChatMiscIntegrationTest.php +++ b/tests/Integration/ChatMiscIntegrationTest.php @@ -16,16 +16,14 @@ #[Group('integration')] class ChatMiscIntegrationTest extends ChatTestCase { - protected static function sharedUserCount(): int - { - return 2; - } - // ========================================================================= // Devices // ========================================================================= - public function testCreateListDeleteDevice(): void + /** + * @test + */ + public function createListDeleteDevice(): void { $userID = $this->getSharedUserIDs()[0]; $deviceID = 'integration-test-device-' . $this->randomString(12); @@ -40,8 +38,9 @@ public function testCreateListDeleteDevice(): void $this->assertResponseSuccess($resp, 'create device'); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'no push providers configured') || str_contains($e->getMessage(), 'push')) { - $this->markTestSkipped('Push providers not configured for this app'); + self::markTestSkipped('Push providers not configured for this app'); } + throw $e; } @@ -53,7 +52,7 @@ public function testCreateListDeleteDevice(): void foreach ($listResp->getData()->devices ?? [] as $device) { if ($device->id === $deviceID) { $found = true; - self::assertEquals('firebase', $device->pushProvider); + self::assertSame('firebase', $device->pushProvider); } } self::assertTrue($found, 'Created device should appear in list'); @@ -67,7 +66,7 @@ public function testCreateListDeleteDevice(): void $this->assertResponseSuccess($listResp2, 'list devices after delete'); foreach ($listResp2->getData()->devices ?? [] as $device) { - self::assertNotEquals($deviceID, $device->id, 'Device should be deleted'); + self::assertNotSame($deviceID, $device->id, 'Device should be deleted'); } } @@ -75,7 +74,10 @@ public function testCreateListDeleteDevice(): void // Blocklists // ========================================================================= - public function testCreateListDeleteBlocklist(): void + /** + * @test + */ + public function createListDeleteBlocklist(): void { $blocklistName = 'test-blocklist-' . $this->randomString(8); @@ -105,7 +107,7 @@ public function testCreateListDeleteBlocklist(): void // Get blocklist $getResp = $this->client->getBlockList($blocklistName, ''); $this->assertResponseSuccess($getResp, 'get blocklist'); - self::assertEquals($blocklistName, $getResp->getData()->blocklist->name); + self::assertSame($blocklistName, $getResp->getData()->blocklist->name); // Delete blocklist $delResp = $this->client->deleteBlockList($blocklistName, ''); @@ -116,6 +118,7 @@ public function testCreateListDeleteBlocklist(): void $this->client->deleteBlockList($blocklistName, ''); } catch (\Exception $ignore) { } + throw $e; } } @@ -124,7 +127,10 @@ public function testCreateListDeleteBlocklist(): void // Commands // ========================================================================= - public function testCreateListDeleteCommand(): void + /** + * @test + */ + public function createListDeleteCommand(): void { $cmdName = 'testcmd' . $this->randomString(6); @@ -136,13 +142,13 @@ public function testCreateListDeleteCommand(): void )); $this->assertResponseSuccess($createResp, 'create command'); self::assertNotNull($createResp->getData()->command); - self::assertEquals($cmdName, $createResp->getData()->command->name); + self::assertSame($cmdName, $createResp->getData()->command->name); // Get command $getResp = $this->getCommand($cmdName); $this->assertResponseSuccess($getResp, 'get command'); - self::assertEquals($cmdName, $getResp->getData()->name); - self::assertEquals('A test command', $getResp->getData()->description); + self::assertSame($cmdName, $getResp->getData()->name); + self::assertSame('A test command', $getResp->getData()->description); // Commands are eventually consistent sleep(2); @@ -162,13 +168,14 @@ public function testCreateListDeleteCommand(): void // Delete command $delResp = $this->deleteCommandApi($cmdName); $this->assertResponseSuccess($delResp, 'delete command'); - self::assertEquals($cmdName, $delResp->getData()->name); + self::assertSame($cmdName, $delResp->getData()->name); } catch (\Exception $e) { // Clean up on failure try { $this->deleteCommandApi($cmdName); } catch (\Exception $ignore) { } + throw $e; } } @@ -177,7 +184,10 @@ public function testCreateListDeleteCommand(): void // Channel Types // ========================================================================= - public function testCreateUpdateDeleteChannelType(): void + /** + * @test + */ + public function createUpdateDeleteChannelType(): void { $typeName = 'testtype' . $this->randomString(6); @@ -190,16 +200,17 @@ public function testCreateUpdateDeleteChannelType(): void maxMessageLength: 3000, )); $this->assertResponseSuccess($createResp, 'create channel type'); - self::assertEquals($typeName, $createResp->getData()->name); - self::assertEquals(3000, $createResp->getData()->maxMessageLength); + self::assertSame($typeName, $createResp->getData()->name); + self::assertSame(3000, $createResp->getData()->maxMessageLength); // Channel types are eventually consistent - retry with delay $getResp = $this->retryUntilSuccess(function () use ($typeName) { $resp = $this->getChannelType($typeName); $this->assertResponseSuccess($resp, 'get channel type'); + return $resp; }, maxAttempts: 10, sleepMs: 2000); - self::assertEquals($typeName, $getResp->getData()->name); + self::assertSame($typeName, $getResp->getData()->name); // Update channel type — retry until the response reflects the updated // value, because the backend may return stale data from its local @@ -212,8 +223,9 @@ public function testCreateUpdateDeleteChannelType(): void typingEvents: false, )); $this->assertResponseSuccess($resp, 'update channel type'); - self::assertEquals(4000, $resp->getData()->maxMessageLength); + self::assertSame(4000, $resp->getData()->maxMessageLength); self::assertFalse($resp->getData()->typingEvents); + return $resp; }, maxAttempts: 5, sleepMs: 2000); @@ -223,6 +235,7 @@ public function testCreateUpdateDeleteChannelType(): void try { $this->deleteChannelType($typeName); $deleteErr = null; + break; } catch (\Exception $e) { $deleteErr = $e; @@ -238,11 +251,15 @@ public function testCreateUpdateDeleteChannelType(): void $this->deleteChannelType($typeName); } catch (\Exception $ignore) { } + throw $e; } } - public function testListChannelTypes(): void + /** + * @test + */ + public function listsChannelTypes(): void { $resp = $this->listChannelTypes(); $this->assertResponseSuccess($resp, 'list channel types'); @@ -256,7 +273,10 @@ public function testListChannelTypes(): void // Permissions // ========================================================================= - public function testListPermissions(): void + /** + * @test + */ + public function listPermissions(): void { $resp = $this->client->listPermissions(); $this->assertResponseSuccess($resp, 'list permissions'); @@ -264,19 +284,25 @@ public function testListPermissions(): void self::assertNotEmpty($resp->getData()->permissions, 'Should have at least one permission'); } - public function testCreatePermission(): void + /** + * @test + */ + public function createPermission(): void { // CreatePermission is hidden from the generated spec (Ignore: true in backend) // per Go SDK reference. Skip this test. - $this->markTestSkipped('CreatePermission is not available in the generated SDK'); + self::markTestSkipped('CreatePermission is not available in the generated SDK'); } - public function testGetPermission(): void + /** + * @test + */ + public function getPermission(): void { $resp = $this->client->getPermission('create-channel'); $this->assertResponseSuccess($resp, 'get permission'); - self::assertEquals('create-channel', $resp->getData()->permission->id); + self::assertSame('create-channel', $resp->getData()->permission->id); self::assertNotEmpty($resp->getData()->permission->action); } @@ -284,7 +310,10 @@ public function testGetPermission(): void // Query Banned Users // ========================================================================= - public function testQueryBannedUsers(): void + /** + * @test + */ + public function queriesBannedUsers(): void { $shared = $this->getSharedUserIDs(); $adminID = $shared[0]; @@ -325,7 +354,10 @@ public function testQueryBannedUsers(): void // Mute / Unmute User // ========================================================================= - public function testMuteUnmuteUser(): void + /** + * @test + */ + public function muteUnmuteUser(): void { $shared = $this->getSharedUserIDs(); $muterID = $shared[0]; @@ -356,7 +388,10 @@ public function testMuteUnmuteUser(): void // App Settings // ========================================================================= - public function testGetAppSettings(): void + /** + * @test + */ + public function getAppSettings(): void { $resp = $this->client->getApp(); $this->assertResponseSuccess($resp, 'get app settings'); @@ -368,7 +403,10 @@ public function testGetAppSettings(): void // Export Channels // ========================================================================= - public function testExportChannels(): void + /** + * @test + */ + public function exportsChannels(): void { $userID = $this->getSharedUserIDs()[0]; @@ -385,14 +423,17 @@ public function testExportChannels(): void // Wait for the export task to complete $taskResult = $this->waitForTask($exportResp->getData()->taskID); - self::assertEquals('completed', $taskResult->status); + self::assertSame('completed', $taskResult->status); } // ========================================================================= // Threads // ========================================================================= - public function testThreads(): void + /** + * @test + */ + public function threads(): void { $shared = $this->getSharedUserIDs(); $userID = $shared[0]; @@ -436,7 +477,7 @@ public function testThreads(): void foreach ($resp->getData()->threads as $thread) { if ($thread->parentMessageID === $parentID) { $found = true; - self::assertEquals($userID2, $thread->createdByUserID); + self::assertSame($userID2, $thread->createdByUserID); } } self::assertTrue($found, "Thread should appear in query results for channel {$channelCID}"); @@ -446,7 +487,10 @@ public function testThreads(): void // Unread Counts // ========================================================================= - public function testGetUnreadCounts(): void + /** + * @test + */ + public function getsUnreadCounts(): void { $shared = $this->getSharedUserIDs(); $userID = $shared[0]; @@ -460,7 +504,10 @@ public function testGetUnreadCounts(): void self::assertGreaterThanOrEqual(0, $resp->getData()->totalUnreadCount); } - public function testGetUnreadCountsBatch(): void + /** + * @test + */ + public function getsUnreadCountsBatch(): void { $shared = $this->getSharedUserIDs(); $userID = $shared[0]; @@ -482,7 +529,10 @@ public function testGetUnreadCountsBatch(): void // Reminders // ========================================================================= - public function testReminders(): void + /** + * @test + */ + public function reminders(): void { $userID = $this->getSharedUserIDs()[0]; @@ -491,6 +541,7 @@ public function testReminders(): void // Create a reminder $remindAt = new \DateTime('+24 hours'); + try { $createResp = $this->createReminder($msgID, new GeneratedModels\CreateReminderRequest( remindAt: $remindAt, @@ -499,13 +550,14 @@ public function testReminders(): void $this->assertResponseSuccess($createResp, 'create reminder'); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'not enabled') || str_contains($e->getMessage(), 'reminder')) { - $this->markTestSkipped('Reminders are not enabled for this app'); + self::markTestSkipped('Reminders are not enabled for this app'); } + throw $e; } - self::assertEquals($msgID, $createResp->getData()->messageID); - self::assertEquals($userID, $createResp->getData()->userID); + self::assertSame($msgID, $createResp->getData()->messageID); + self::assertSame($userID, $createResp->getData()->userID); self::assertNotNull($createResp->getData()->remindAt, 'RemindAt should be set'); // Update reminder @@ -515,7 +567,7 @@ public function testReminders(): void userID: $userID, )); $this->assertResponseSuccess($updateResp, 'update reminder'); - self::assertEquals($msgID, $updateResp->getData()->reminder->messageID); + self::assertSame($msgID, $updateResp->getData()->reminder->messageID); // Query reminders $qResp = $this->queryReminders(new GeneratedModels\QueryRemindersRequest( @@ -525,7 +577,7 @@ public function testReminders(): void )); $this->assertResponseSuccess($qResp, 'query reminders'); self::assertNotEmpty($qResp->getData()->reminders, 'Should find the reminder'); - self::assertEquals($msgID, $qResp->getData()->reminders[0]->messageID); + self::assertSame($msgID, $qResp->getData()->reminders[0]->messageID); // Delete reminder $delResp = $this->deleteReminderApi($msgID, $userID); @@ -536,7 +588,10 @@ public function testReminders(): void // Send User Custom Event // ========================================================================= - public function testSendUserCustomEvent(): void + /** + * @test + */ + public function sendsUserCustomEvent(): void { $userID = $this->getSharedUserIDs()[0]; @@ -553,7 +608,10 @@ public function testSendUserCustomEvent(): void // Query Team Usage Stats // ========================================================================= - public function testQueryTeamUsageStats(): void + /** + * @test + */ + public function queriesTeamUsageStats(): void { try { $resp = $this->queryTeamUsageStats(new GeneratedModels\QueryTeamUsageStatsRequest()); @@ -561,8 +619,9 @@ public function testQueryTeamUsageStats(): void self::assertNotNull($resp->getData()->teams); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'Token signature is invalid') || str_contains($e->getMessage(), 'not available')) { - $this->markTestSkipped('QueryTeamUsageStats not available on this app'); + self::markTestSkipped('QueryTeamUsageStats not available on this app'); } + throw $e; } } @@ -571,10 +630,18 @@ public function testQueryTeamUsageStats(): void // Channel Batch Update // ========================================================================= - public function testChannelBatchUpdate(): void + /** + * @test + */ + public function channelBatchUpdate(): void { // ChannelBatchUpdate is behind Ignore+Beta in the backend spec, // so the generated SDK doesn't include it yet. Per Go SDK reference. - $this->markTestSkipped('ChannelBatchUpdate is not yet available in the generated SDK'); + self::markTestSkipped('ChannelBatchUpdate is not yet available in the generated SDK'); + } + + protected static function sharedUserCount(): int + { + return 2; } } diff --git a/tests/Integration/ChatModerationIntegrationTest.php b/tests/Integration/ChatModerationIntegrationTest.php index d489532..6c9e43e 100644 --- a/tests/Integration/ChatModerationIntegrationTest.php +++ b/tests/Integration/ChatModerationIntegrationTest.php @@ -19,16 +19,14 @@ #[Group('integration')] class ChatModerationIntegrationTest extends ChatTestCase { - protected static function sharedUserCount(): int - { - return 4; - } - // ========================================================================= // Ban / Unban // ========================================================================= - public function testBanUnbanUser(): void + /** + * @test + */ + public function banUnbanUser(): void { $shared = $this->getSharedUserIDs(); $userIDs = [$shared[0], $shared[1], $shared[2]]; @@ -55,7 +53,7 @@ public function testBanUnbanUser(): void self::assertNotEmpty($qResp->getData()->bans, 'Should find the banned user'); $ban = $qResp->getData()->bans[0]; - self::assertEquals('test ban reason', $ban->reason); + self::assertSame('test ban reason', $ban->reason); // When timeout is set, expires should be populated self::assertNotNull($ban->expires, 'Ban with timeout should have Expires set'); @@ -100,7 +98,10 @@ public function testBanUnbanUser(): void // Mute / Unmute // ========================================================================= - public function testMuteUnmuteUser(): void + /** + * @test + */ + public function muteUnmuteUser(): void { $userIDs = $this->getSharedUserIDs(); $muterID = $userIDs[0]; @@ -157,7 +158,10 @@ public function testMuteUnmuteUser(): void // Flag // ========================================================================= - public function testFlagMessageAndUser(): void + /** + * @test + */ + public function flagMessageAndUser(): void { $shared = $this->getSharedUserIDs(); $userID = $shared[0]; @@ -204,4 +208,9 @@ public function testFlagMessageAndUser(): void $this->assertResponseSuccess($flagUserResp, 'flag user'); self::assertNotEmpty($flagUserResp->getData()->itemID, 'Flag user should return an item ID'); } + + protected static function sharedUserCount(): int + { + return 4; + } } diff --git a/tests/Integration/ChatPollsIntegrationTest.php b/tests/Integration/ChatPollsIntegrationTest.php index 4f600cb..59a2b74 100644 --- a/tests/Integration/ChatPollsIntegrationTest.php +++ b/tests/Integration/ChatPollsIntegrationTest.php @@ -5,7 +5,6 @@ namespace GetStream\Tests\Integration; use GetStream\GeneratedModels; -use GetStream\StreamResponse; use PHPUnit\Framework\Attributes\Group; /** @@ -21,11 +20,6 @@ class ChatPollsIntegrationTest extends ChatTestCase /** @var string|null User ID to use for poll cleanup */ private ?string $pollCleanupUserID = null; - protected static function sharedUserCount(): int - { - return 2; - } - protected function tearDown(): void { // Delete polls before users/channels @@ -42,8 +36,10 @@ protected function tearDown(): void /** * Test creating a poll with options, querying it by ID, and verifying the results. + * + * @test */ - public function testCreateAndQueryPoll(): void + public function createAndQueryPoll(): void { $userIDs = [$this->getSharedUserIDs()[0]]; $this->pollCleanupUserID = $userIDs[0]; @@ -63,20 +59,21 @@ public function testCreateAndQueryPoll(): void )); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'Polls are not enabled') || str_contains($e->getMessage(), 'polls') && str_contains($e->getMessage(), 'not enabled')) { - $this->markTestSkipped('Polls feature not enabled for this app'); + self::markTestSkipped('Polls feature not enabled for this app'); } + throw $e; } $this->assertResponseSuccess($createResp, 'create poll'); $poll = $createResp->getData()->poll; - $this->assertNotNull($poll); - $this->assertNotEmpty($poll->id); - $this->assertEquals('Favorite color?', $poll->name); - $this->assertEquals('Pick your favorite color', $poll->description); - $this->assertTrue($poll->enforceUniqueVote); - $this->assertNotNull($poll->options); - $this->assertCount(3, $poll->options); + self::assertNotNull($poll); + self::assertNotEmpty($poll->id); + self::assertSame('Favorite color?', $poll->name); + self::assertSame('Pick your favorite color', $poll->description); + self::assertTrue($poll->enforceUniqueVote); + self::assertNotNull($poll->options); + self::assertCount(3, $poll->options); $pollID = $poll->id; $this->createdPollIDs[] = $pollID; @@ -84,31 +81,34 @@ public function testCreateAndQueryPoll(): void // Get the poll by ID $getResp = $this->getPoll($pollID); $this->assertResponseSuccess($getResp, 'get poll'); - $this->assertEquals($pollID, $getResp->getData()->poll->id); - $this->assertEquals('Favorite color?', $getResp->getData()->poll->name); + self::assertSame($pollID, $getResp->getData()->poll->id); + self::assertSame('Favorite color?', $getResp->getData()->poll->name); // Query polls with filter by ID $queryResp = $this->queryPolls(new GeneratedModels\QueryPollsRequest( filter: (object) ['id' => $pollID], ), $userIDs[0]); $this->assertResponseSuccess($queryResp, 'query polls'); - $this->assertNotNull($queryResp->getData()->polls); - $this->assertGreaterThanOrEqual(1, count($queryResp->getData()->polls)); + self::assertNotNull($queryResp->getData()->polls); + self::assertGreaterThanOrEqual(1, count($queryResp->getData()->polls)); $found = false; foreach ($queryResp->getData()->polls as $p) { if ($p->id === $pollID) { $found = true; + break; } } - $this->assertTrue($found, 'Poll should be found in query results'); + self::assertTrue($found, 'Poll should be found in query results'); } /** * Test creating a poll, attaching it to a message, and casting a vote. + * + * @test */ - public function testCastPollVote(): void + public function castsPollVote(): void { $userIDs = $this->getSharedUserIDs(); $this->pollCleanupUserID = $userIDs[0]; @@ -126,21 +126,22 @@ public function testCastPollVote(): void )); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'Polls are not enabled') || str_contains($e->getMessage(), 'polls') && str_contains($e->getMessage(), 'not enabled')) { - $this->markTestSkipped('Polls feature not enabled for this app'); + self::markTestSkipped('Polls feature not enabled for this app'); } + throw $e; } $this->assertResponseSuccess($createResp, 'create vote test poll'); $poll = $createResp->getData()->poll; - $this->assertNotNull($poll); + self::assertNotNull($poll); $pollID = $poll->id; $this->createdPollIDs[] = $pollID; - $this->assertNotNull($poll->options); - $this->assertGreaterThanOrEqual(2, count($poll->options)); + self::assertNotNull($poll->options); + self::assertGreaterThanOrEqual(2, count($poll->options)); $optionID = $poll->options[0]->id; - $this->assertNotEmpty($optionID); + self::assertNotEmpty($optionID); // Create a channel and send a message with the poll attached [$type, $id] = $this->createTestChannelWithMembers($userIDs[0], $userIDs); @@ -155,13 +156,14 @@ public function testCastPollVote(): void )); } catch (\Exception $e) { if (str_contains($e->getMessage(), 'polls not enabled') || str_contains($e->getMessage(), 'Polls are not enabled')) { - $this->markTestSkipped('Polls not enabled for this channel type'); + self::markTestSkipped('Polls not enabled for this channel type'); } + throw $e; } $this->assertResponseSuccess($msgResp, 'send message with poll'); $msgID = $msgResp->getData()->message->id; - $this->assertNotEmpty($msgID); + self::assertNotEmpty($msgID); // Cast a vote from the second user $voteResp = $this->castPollVote($msgID, $pollID, new GeneratedModels\CastPollVoteRequest( @@ -171,13 +173,18 @@ public function testCastPollVote(): void ), )); $this->assertResponseSuccess($voteResp, 'cast poll vote'); - $this->assertNotNull($voteResp->getData()->vote); - $this->assertEquals($optionID, $voteResp->getData()->vote->optionID); + self::assertNotNull($voteResp->getData()->vote); + self::assertSame($optionID, $voteResp->getData()->vote->optionID); // Verify the poll has votes by getting it again $getResp = $this->getPoll($pollID); $this->assertResponseSuccess($getResp, 'get poll after vote'); - $this->assertNotNull($getResp->getData()->poll); - $this->assertEquals(1, $getResp->getData()->poll->voteCount); + self::assertNotNull($getResp->getData()->poll); + self::assertSame(1, $getResp->getData()->poll->voteCount); + } + + protected static function sharedUserCount(): int + { + return 2; } } diff --git a/tests/Integration/ChatReactionIntegrationTest.php b/tests/Integration/ChatReactionIntegrationTest.php index 8f5831c..fb92a2d 100644 --- a/tests/Integration/ChatReactionIntegrationTest.php +++ b/tests/Integration/ChatReactionIntegrationTest.php @@ -5,7 +5,6 @@ namespace GetStream\Tests\Integration; use GetStream\GeneratedModels; -use GetStream\StreamResponse; use PHPUnit\Framework\Attributes\Group; /** @@ -15,15 +14,12 @@ #[Group('integration')] class ChatReactionIntegrationTest extends ChatTestCase { - protected static function sharedUserCount(): int - { - return 2; - } - /** * Test sending a reaction and retrieving reactions on a message. + * + * @test */ - public function testSendAndGetReactions(): void + public function sendAndGetReactions(): void { $userIDs = $this->getSharedUserIDs(); [$type, $id] = $this->createTestChannelWithMembers($userIDs[0], $userIDs); @@ -37,9 +33,9 @@ public function testSendAndGetReactions(): void ), )); $this->assertResponseSuccess($resp, 'send like reaction'); - $this->assertNotNull($resp->getData()->reaction); - $this->assertEquals('like', $resp->getData()->reaction->type); - $this->assertEquals($userIDs[0], $resp->getData()->reaction->userID); + self::assertNotNull($resp->getData()->reaction); + self::assertSame('like', $resp->getData()->reaction->type); + self::assertSame($userIDs[0], $resp->getData()->reaction->userID); // Send a "love" reaction from user 2 $resp = $this->sendReaction($msgID, new GeneratedModels\SendReactionRequest( @@ -49,19 +45,21 @@ public function testSendAndGetReactions(): void ), )); $this->assertResponseSuccess($resp, 'send love reaction'); - $this->assertEquals('love', $resp->getData()->reaction->type); + self::assertSame('love', $resp->getData()->reaction->type); // Get reactions and verify both are present $getResp = $this->getReactions($msgID); $this->assertResponseSuccess($getResp, 'get reactions'); - $this->assertNotNull($getResp->getData()->reactions); - $this->assertGreaterThanOrEqual(2, count($getResp->getData()->reactions)); + self::assertNotNull($getResp->getData()->reactions); + self::assertGreaterThanOrEqual(2, count($getResp->getData()->reactions)); } /** * Test deleting a reaction from a message. + * + * @test */ - public function testDeleteReaction(): void + public function deletesReaction(): void { $userIDs = [$this->getSharedUserIDs()[0]]; [$type, $id] = $this->createTestChannelWithMembers($userIDs[0], $userIDs); @@ -87,15 +85,17 @@ public function testDeleteReaction(): void $reactions = $getResp->getData()->reactions ?? []; foreach ($reactions as $r) { if ($r->userID === $userIDs[0]) { - $this->assertNotEquals('like', $r->type, 'Like reaction should have been deleted'); + self::assertNotSame('like', $r->type, 'Like reaction should have been deleted'); } } } /** * Test enforce_unique: sending a second reaction with enforce_unique replaces the first. + * + * @test */ - public function testEnforceUniqueReaction(): void + public function enforceUniqueReaction(): void { $userIDs = [$this->getSharedUserIDs()[0]]; [$type, $id] = $this->createTestChannelWithMembers($userIDs[0], $userIDs); @@ -132,6 +132,11 @@ public function testEnforceUniqueReaction(): void $userReactions++; } } - $this->assertEquals(1, $userReactions, 'EnforceUnique should keep only one reaction per user'); + self::assertSame(1, $userReactions, 'EnforceUnique should keep only one reaction per user'); + } + + protected static function sharedUserCount(): int + { + return 2; } } diff --git a/tests/Integration/ChatTestCase.php b/tests/Integration/ChatTestCase.php index be138cb..d2f8e4f 100644 --- a/tests/Integration/ChatTestCase.php +++ b/tests/Integration/ChatTestCase.php @@ -6,7 +6,6 @@ use GetStream\Client; use GetStream\ClientBuilder; -use GetStream\Exceptions\StreamApiException; use GetStream\GeneratedModels; use GetStream\StreamResponse; use PHPUnit\Framework\Attributes\Group; @@ -32,6 +31,19 @@ abstract class ChatTestCase extends TestCase /** @var array{type: string, id: string}[] Channels created during the test, cleaned up in tearDown */ protected array $createdChannels = []; + // ========================================================================= + // Video API Wrappers + // ========================================================================= + + /** @var string[] Call type names created during the test, cleaned up in tearDown */ + protected array $createdCallTypes = []; + + /** @var array{type: string, id: string}[] Calls created during the test, cleaned up in tearDown */ + protected array $createdCalls = []; + + /** @var string[] External storage names created during the test, cleaned up in tearDown */ + protected array $createdExternalStorages = []; + // ------------------------------------------------------------------ // Class-level shared user management (created once per test class) // ------------------------------------------------------------------ @@ -42,15 +54,6 @@ abstract class ChatTestCase extends TestCase /** @var array Class-level shared user IDs keyed by class name */ private static array $classUserIDs = []; - /** - * Override to declare how many shared users this test class needs. - * Return 0 (default) to opt-out and create users per-test instead. - */ - protected static function sharedUserCount(): int - { - return 0; - } - public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); @@ -93,50 +96,6 @@ public static function tearDownAfterClass(): void parent::tearDownAfterClass(); } - /** - * Delete users with retry and jitter to avoid the DeleteUsers rate limit (6/min). - * With 8 parallel paratest workers all tearing down at the same time, a burst - * of concurrent calls easily exceeds the limit. A random initial jitter spreads - * the calls across the minute window before any retry backoff kicks in. - * - * @param string[] $userIDs - */ - private static function deleteUsersHardWithRetry(Client $client, array $userIDs): void - { - // Jitter 0-4s to spread concurrent teardown calls from parallel workers - usleep(random_int(0, 4000000)); - - for ($i = 0; $i < 8; $i++) { - try { - $client->deleteUsers(new GeneratedModels\DeleteUsersRequest( - userIds: $userIDs, - user: 'hard', - messages: 'hard', - conversations: 'hard', - )); - return; - } catch (\Exception $e) { - if (strpos($e->getMessage(), 'Too many requests') === false && - strpos($e->getMessage(), '429') === false) { - // Non-rate-limit error during cleanup — ignore it - return; - } - // Exponential backoff: 2s, 4s, 8s, 16s … capped at 30s - sleep(min(2 ** ($i + 1), 30)); - } - } - } - - /** - * Get the class-level shared user IDs. - * - * @return string[] - */ - protected function getSharedUserIDs(): array - { - return self::$classUserIDs[static::class] ?? []; - } - // ------------------------------------------------------------------ // Per-test setup / teardown // ------------------------------------------------------------------ @@ -166,6 +125,25 @@ protected function tearDown(): void parent::tearDown(); } + /** + * Override to declare how many shared users this test class needs. + * Return 0 (default) to opt-out and create users per-test instead. + */ + protected static function sharedUserCount(): int + { + return 0; + } + + /** + * Get the class-level shared user IDs. + * + * @return string[] + */ + protected function getSharedUserIDs(): array + { + return self::$classUserIDs[static::class] ?? []; + } + // ========================================================================= // Test Helpers // ========================================================================= @@ -227,6 +205,7 @@ protected function createTestChannel(string $creatorID): array * Create a messaging channel with members. * * @param string[] $memberIDs + * * @return array{0: string, 1: string} [channelType, channelID] */ protected function createTestChannelWithMembers(string $creatorID, array $memberIDs): array @@ -235,7 +214,7 @@ protected function createTestChannelWithMembers(string $creatorID, array $member $channelID = 'test-ch-' . uniqid(); $members = array_map( - fn(string $id) => new GeneratedModels\ChannelMemberRequest(userID: $id), + static fn (string $id) => new GeneratedModels\ChannelMemberRequest(userID: $id), $memberIDs, ); @@ -280,9 +259,10 @@ protected function sendTestMessage(string $channelType, string $channelID, strin * Useful for eventually consistent API operations. * * @template T + * * @param callable(): T $fn - * @param int $maxAttempts - * @param int $sleepMs milliseconds to wait between attempts + * @param int $sleepMs milliseconds to wait between attempts + * * @return T */ protected function retryUntilSuccess(callable $fn, int $maxAttempts = 5, int $sleepMs = 500): mixed @@ -296,6 +276,7 @@ protected function retryUntilSuccess(callable $fn, int $maxAttempts = 5, int $sl usleep($sleepMs * 1000); } } + throw $lastException; } @@ -524,6 +505,7 @@ protected function updateMemberPartial(string $type, string $id, string $userID, protected function queryMembers(GeneratedModels\QueryMembersPayload $payload): StreamResponse { $queryParams = ['payload' => json_encode($payload->toArray())]; + return StreamResponse::fromJson( $this->client->makeRequest('GET', '/api/v2/chat/members', $queryParams), GeneratedModels\MembersResponse::class, @@ -1007,6 +989,7 @@ protected function flagContent(GeneratedModels\FlagRequest $request): StreamResp protected function queryMessageFlags(GeneratedModels\QueryMessageFlagsPayload $payload): StreamResponse { $queryParams = ['payload' => json_encode($payload->toArray())]; + return StreamResponse::fromJson( $this->client->makeRequest('GET', '/api/v2/chat/moderation/flags/message', $queryParams), GeneratedModels\QueryMessageFlagsResponse::class, @@ -1147,19 +1130,6 @@ protected function queryTeamUsageStats(GeneratedModels\QueryTeamUsageStatsReques ); } - // ========================================================================= - // Video API Wrappers - // ========================================================================= - - /** @var string[] Call type names created during the test, cleaned up in tearDown */ - protected array $createdCallTypes = []; - - /** @var array{type: string, id: string}[] Calls created during the test, cleaned up in tearDown */ - protected array $createdCalls = []; - - /** @var string[] External storage names created during the test, cleaned up in tearDown */ - protected array $createdExternalStorages = []; - /** * @return StreamResponse */ @@ -1398,8 +1368,6 @@ protected function cleanupVideoResources(): void /** * Create a call and track it for cleanup. - * - * @return GeneratedModels\GetOrCreateCallResponse */ protected function createTrackedCall(string $type, string $id, string $creatorID): GeneratedModels\GetOrCreateCallResponse { @@ -1410,6 +1378,7 @@ protected function createTrackedCall(string $type, string $id, string $creatorID )); $this->assertResponseSuccess($response, 'create call'); $this->createdCalls[] = ['type' => $type, 'id' => $id]; + return $response->getData(); } @@ -1424,4 +1393,39 @@ protected function assertResponseSuccess(StreamResponse $response, string $opera "Failed to {$operation}. Status: {$response->getStatusCode()}, Body: {$response->getRawBody()}" ); } + + /** + * Delete users with retry and jitter to avoid the DeleteUsers rate limit (6/min). + * With 8 parallel paratest workers all tearing down at the same time, a burst + * of concurrent calls easily exceeds the limit. A random initial jitter spreads + * the calls across the minute window before any retry backoff kicks in. + * + * @param string[] $userIDs + */ + private static function deleteUsersHardWithRetry(Client $client, array $userIDs): void + { + // Jitter 0-4s to spread concurrent teardown calls from parallel workers + usleep(random_int(0, 4000000)); + + for ($i = 0; $i < 8; $i++) { + try { + $client->deleteUsers(new GeneratedModels\DeleteUsersRequest( + userIds: $userIDs, + user: 'hard', + messages: 'hard', + conversations: 'hard', + )); + + return; + } catch (\Exception $e) { + if (strpos($e->getMessage(), 'Too many requests') === false + && strpos($e->getMessage(), '429') === false) { + // Non-rate-limit error during cleanup — ignore it + return; + } + // Exponential backoff: 2s, 4s, 8s, 16s … capped at 30s + sleep(min(2 ** ($i + 1), 30)); + } + } + } } diff --git a/tests/Integration/ChatUserIntegrationTest.php b/tests/Integration/ChatUserIntegrationTest.php index 58d2620..8702e45 100644 --- a/tests/Integration/ChatUserIntegrationTest.php +++ b/tests/Integration/ChatUserIntegrationTest.php @@ -48,9 +48,9 @@ public function testQueryUsers(): void self::assertNotNull($data->users); self::assertGreaterThanOrEqual(2, count($data->users)); - $foundIDs = array_map(fn($u) => $u->id, $data->users); + $foundIDs = array_map(static fn ($u) => $u->id, $data->users); foreach ($userIDs as $id) { - self::assertContains($id, $foundIDs, "User $id should be found in query results"); + self::assertContains($id, $foundIDs, "User {$id} should be found in query results"); } } @@ -132,7 +132,7 @@ public function testBlockUnblockUser(): void $this->assertResponseSuccess($blockedResp, 'get blocked users'); $blocks = $blockedResp->getData()->blocks; self::assertNotNull($blocks); - $blockedIDs = array_map(fn($b) => $b->blockedUserID, $blocks); + $blockedIDs = array_map(static fn ($b) => $b->blockedUserID, $blocks); self::assertContains($bob, $blockedIDs, 'Bob should be in blocked list'); // Unblock bob @@ -146,7 +146,7 @@ public function testBlockUnblockUser(): void $blockedResp = $this->client->getBlockedUsers($alice); $this->assertResponseSuccess($blockedResp, 'get blocked users after unblock'); $blocks = $blockedResp->getData()->blocks ?? []; - $blockedIDs = array_map(fn($b) => $b->blockedUserID, $blocks); + $blockedIDs = array_map(static fn ($b) => $b->blockedUserID, $blocks); self::assertNotContains($bob, $blockedIDs, 'Bob should not be in blocked list after unblock'); } @@ -202,6 +202,7 @@ public function testDeleteUsers(): void conversations: 'hard', )); $this->assertResponseSuccess($resp, 'delete users'); + return $resp; }, maxAttempts: 5, sleepMs: 3000); @@ -210,7 +211,7 @@ public function testDeleteUsers(): void // Wait for task to complete $taskResult = $this->waitForTask($taskID); - self::assertEquals('completed', $taskResult->status, 'Delete task should complete'); + self::assertSame('completed', $taskResult->status, 'Delete task should complete'); } /** @@ -242,7 +243,7 @@ public function testCreateGuest(): void )); } catch (\Exception $e) { // Guest access may be disabled for this app - $this->markTestSkipped('Guest user creation not enabled: ' . $e->getMessage()); + self::markTestSkipped('Guest user creation not enabled: ' . $e->getMessage()); } $this->assertResponseSuccess($response, 'create guest'); @@ -282,9 +283,9 @@ public function testUpsertUsersWithRoleAndTeamsRole(): void self::assertArrayHasKey($userID, $data->users); $user = $data->users[$userID]; - self::assertEquals('admin', $user->role); - self::assertEquals(['blue'], $user->teams); - self::assertEquals(['blue' => 'admin'], $user->teamsRole); + self::assertSame('admin', $user->role); + self::assertSame(['blue'], $user->teams); + self::assertSame(['blue' => 'admin'], $user->teamsRole); } /** @@ -313,8 +314,8 @@ public function testPartialUpdateUserWithTeam(): void self::assertArrayHasKey($userID, $data->users); $user = $data->users[$userID]; - self::assertEquals(['blue'], $user->teams); - self::assertEquals(['blue' => 'admin'], $user->teamsRole); + self::assertSame(['blue'], $user->teams); + self::assertSame(['blue' => 'admin'], $user->teamsRole); } /** @@ -468,7 +469,7 @@ public function testDeactivateUsersPlural(): void self::assertNotEmpty($taskID, 'Task ID should not be empty'); $taskResult = $this->waitForTask($taskID); - self::assertEquals('completed', $taskResult->status, 'Deactivate task should complete'); + self::assertSame('completed', $taskResult->status, 'Deactivate task should complete'); // Verify both are deactivated — query without flag should return 0 $queryResp = $this->client->queryUsers(new GeneratedModels\QueryUsersPayload( diff --git a/tests/Integration/FeedBookmarkFollowIntegrationTest.php b/tests/Integration/FeedBookmarkFollowIntegrationTest.php index 94347b4..eb5c25c 100644 --- a/tests/Integration/FeedBookmarkFollowIntegrationTest.php +++ b/tests/Integration/FeedBookmarkFollowIntegrationTest.php @@ -7,7 +7,6 @@ use GetStream\Client; use GetStream\ClientBuilder; use GetStream\Exceptions\StreamApiException; -use GetStream\Exceptions\StreamException; use GetStream\Feed; use GetStream\FeedsV3Client; use GetStream\GeneratedModels; diff --git a/tests/Integration/VideoIntegrationTest.php b/tests/Integration/VideoIntegrationTest.php index 32ae3c0..0dd8d81 100644 --- a/tests/Integration/VideoIntegrationTest.php +++ b/tests/Integration/VideoIntegrationTest.php @@ -22,104 +22,29 @@ protected function tearDown(): void parent::tearDown(); } - /** - * Helper: create a unique call type and track for cleanup. - */ - private function createTrackedCallType(string $name): GeneratedModels\CreateCallTypeResponse - { - // Clean up excess call types first (same pattern as Go SDK) - try { - $listResp = $this->listCallTypesVideo(); - if ($listResp->isSuccessful() && $listResp->getData()->callTypes !== null && count($listResp->getData()->callTypes) > 10) { - $builtIn = ['default', 'livestream', 'audio_room', 'development']; - foreach ($listResp->getData()->callTypes as $ctName => $ct) { - if (!in_array($ctName, $builtIn, true)) { - try { - $this->deleteCallTypeVideo($ctName); - } catch (\Exception $e) { - // Ignore - } - } - } - } - } catch (\Exception $e) { - // Ignore list errors - } - - $response = $this->createCallTypeVideo(new GeneratedModels\CreateCallTypeRequest( - name: $name, - grants: [ - 'admin' => ['send-audio', 'send-video', 'mute-users'], - 'user' => ['send-audio', 'send-video'], - ], - settings: new GeneratedModels\CallSettingsRequest( - audio: new GeneratedModels\AudioSettingsRequest( - defaultDevice: 'speaker', - micDefaultOn: true, - ), - screensharing: new GeneratedModels\ScreensharingSettingsRequest( - accessRequestEnabled: false, - enabled: true, - ), - ), - notificationSettings: new GeneratedModels\NotificationSettingsRequest( - enabled: true, - callNotification: new GeneratedModels\EventNotificationSettingsRequest( - enabled: true, - apns: new GeneratedModels\APNSPayload( - title: '{{ user.display_name }} invites you to a call', - body: '', - ), - ), - sessionStarted: new GeneratedModels\EventNotificationSettingsRequest( - enabled: false, - apns: new GeneratedModels\APNSPayload( - title: '{{ user.display_name }} invites you to a call', - body: '', - ), - ), - callLiveStarted: new GeneratedModels\EventNotificationSettingsRequest( - enabled: false, - apns: new GeneratedModels\APNSPayload( - title: '{{ user.display_name }} invites you to a call', - body: '', - ), - ), - callRing: new GeneratedModels\EventNotificationSettingsRequest( - enabled: false, - apns: new GeneratedModels\APNSPayload( - title: '{{ user.display_name }} invites you to a call', - body: '', - ), - ), - ), - )); - $this->assertResponseSuccess($response, 'create call type'); - $this->createdCallTypes[] = $name; - return $response->getData(); - } - // ========================================================================= // Tests // ========================================================================= /** * CRUDCallTypeOperations: Create call type with settings, update, read, delete. + * + * @test */ - public function testCRUDCallTypeOperations(): void + public function cRUDCallTypeOperations(): void { $callTypeName = 'calltype-' . $this->randomString(10); $data = $this->createTrackedCallType($callTypeName); // Verify creation - $this->assertEquals($callTypeName, $data->name); - $this->assertTrue($data->settings->audio->micDefaultOn); - $this->assertEquals('speaker', $data->settings->audio->defaultDevice); - $this->assertFalse($data->settings->screensharing->accessRequestEnabled); - $this->assertTrue($data->settings->screensharing->enabled); - $this->assertTrue($data->notificationSettings->enabled); - $this->assertTrue($data->notificationSettings->callNotification->enabled); - $this->assertFalse($data->notificationSettings->sessionStarted->enabled); + self::assertSame($callTypeName, $data->name); + self::assertTrue($data->settings->audio->micDefaultOn); + self::assertSame('speaker', $data->settings->audio->defaultDevice); + self::assertFalse($data->settings->screensharing->accessRequestEnabled); + self::assertTrue($data->settings->screensharing->enabled); + self::assertTrue($data->notificationSettings->enabled); + self::assertTrue($data->notificationSettings->callNotification->enabled); + self::assertFalse($data->notificationSettings->sessionStarted->enabled); // Update call type settings (retry until eventual consistency settles) $updateResp = $this->retryUntilSuccess(fn () => $this->updateCallTypeVideo($callTypeName, new GeneratedModels\UpdateCallTypeRequest( @@ -141,28 +66,30 @@ public function testCRUDCallTypeOperations(): void )), maxAttempts: 10, sleepMs: 1000); $this->assertResponseSuccess($updateResp, 'update call type'); $updated = $updateResp->getData(); - $this->assertFalse($updated->settings->audio->micDefaultOn); - $this->assertEquals('earpiece', $updated->settings->audio->defaultDevice); - $this->assertEquals('disabled', $updated->settings->recording->mode); - $this->assertTrue($updated->settings->backstage->enabled); + self::assertFalse($updated->settings->audio->micDefaultOn); + self::assertSame('earpiece', $updated->settings->audio->defaultDevice); + self::assertSame('disabled', $updated->settings->recording->mode); + self::assertTrue($updated->settings->backstage->enabled); // Read call type $getResp = $this->getCallTypeVideo($callTypeName); $this->assertResponseSuccess($getResp, 'get call type'); - $this->assertEquals($callTypeName, $getResp->getData()->name); + self::assertSame($callTypeName, $getResp->getData()->name); // Delete call type (handled by cleanup, but verify it works) $delResp = $this->deleteCallTypeVideo($callTypeName); $this->assertResponseSuccess($delResp, 'delete call type'); // Remove from tracked list since we already deleted it - $this->createdCallTypes = array_filter($this->createdCallTypes, fn($n) => $n !== $callTypeName); + $this->createdCallTypes = array_filter($this->createdCallTypes, static fn ($n) => $n !== $callTypeName); } /** * CreateCallWithMembers: Create call, add members. + * + * @test */ - public function testCreateCallWithMembers(): void + public function createCallWithMembers(): void { $userIDs = $this->createTestUsers(2); $callID = 'test-call-' . $this->randomString(10); @@ -180,15 +107,17 @@ public function testCreateCallWithMembers(): void $this->createdCalls[] = ['type' => 'default', 'id' => $callID]; $data = $response->getData(); - $this->assertNotNull($data->call); - $this->assertNotNull($data->members); - $this->assertCount(2, $data->members); + self::assertNotNull($data->call); + self::assertNotNull($data->members); + self::assertCount(2, $data->members); } /** * BlockUnblockUserFromCalls: Block user from call, verify; unblock. + * + * @test */ - public function testBlockUnblockUserFromCalls(): void + public function blockUnblockUserFromCalls(): void { $userIDs = $this->createTestUsers(2); $callID = 'test-call-' . $this->randomString(10); @@ -207,7 +136,7 @@ public function testBlockUnblockUserFromCalls(): void $getResp = $this->getCall('default', $callID); $this->assertResponseSuccess($getResp, 'get call after block'); $blockedIds = $getResp->getData()->call->blockedUserIds ?? []; - $this->assertContains($userIDs[1], $blockedIds, 'User should be in blocked list. Got: ' . json_encode($blockedIds)); + self::assertContains($userIDs[1], $blockedIds, 'User should be in blocked list. Got: ' . json_encode($blockedIds)); // Unblock user $unblockResp = $this->unblockUserInCall('default', $callID, new GeneratedModels\UnblockUserRequest( @@ -219,13 +148,15 @@ public function testBlockUnblockUserFromCalls(): void $getResp2 = $this->getCall('default', $callID); $this->assertResponseSuccess($getResp2, 'get call after unblock'); $blocked = $getResp2->getData()->call->blockedUserIds ?? []; - $this->assertNotContains($userIDs[1], $blocked); + self::assertNotContains($userIDs[1], $blocked); } /** * SendCustomEvent: Send custom event in a call. + * + * @test */ - public function testSendCustomEvent(): void + public function sendCustomEvent(): void { $userIDs = $this->createTestUsers(1); $callID = 'test-call-' . $this->randomString(10); @@ -241,8 +172,10 @@ public function testSendCustomEvent(): void /** * MuteAll: Mute all users in a call. + * + * @test */ - public function testMuteAll(): void + public function muteAll(): void { $userIDs = $this->createTestUsers(1); $callID = 'test-call-' . $this->randomString(10); @@ -259,8 +192,10 @@ public function testMuteAll(): void /** * MuteSomeUsers: Mute specific users (audio, video, screenshare). + * + * @test */ - public function testMuteSomeUsers(): void + public function muteSomeUsers(): void { $userIDs = $this->createTestUsers(3); $callID = 'test-call-' . $this->randomString(10); @@ -280,8 +215,10 @@ public function testMuteSomeUsers(): void /** * UpdateUserPermissions: Revoke/grant permissions in a call. + * + * @test */ - public function testUpdateUserPermissions(): void + public function updateUserPermissions(): void { $userIDs = $this->createTestUsers(2); $callID = 'test-call-' . $this->randomString(10); @@ -305,8 +242,10 @@ public function testUpdateUserPermissions(): void /** * DeactivateUser: Deactivate/reactivate users, batch deactivate. + * + * @test */ - public function testDeactivateUser(): void + public function deactivateUser(): void { $userIDs = $this->createTestUsers(2); @@ -324,16 +263,18 @@ public function testDeactivateUser(): void )); $this->assertResponseSuccess($batchResp, 'batch deactivate users'); $taskID = $batchResp->getData()->taskID; - $this->assertNotEmpty($taskID); + self::assertNotEmpty($taskID); $taskResult = $this->waitForTask($taskID); - $this->assertEquals('completed', $taskResult->status); + self::assertSame('completed', $taskResult->status); } /** * CreateCallWithSessionTimer: Create call with max_duration_seconds, update. + * + * @test */ - public function testCreateCallWithSessionTimer(): void + public function createCallWithSessionTimer(): void { $userIDs = $this->createTestUsers(1); $callID = 'test-call-' . $this->randomString(10); @@ -351,7 +292,7 @@ public function testCreateCallWithSessionTimer(): void )); $this->assertResponseSuccess($response, 'create call with session timer'); $this->createdCalls[] = ['type' => 'default', 'id' => $callID]; - $this->assertEquals(3600, $response->getData()->call->settings->limits->maxDurationSeconds); + self::assertSame(3600, $response->getData()->call->settings->limits->maxDurationSeconds); // Update to 7200 $updateResp = $this->updateCall('default', $callID, new GeneratedModels\UpdateCallRequest( @@ -362,7 +303,7 @@ public function testCreateCallWithSessionTimer(): void ), )); $this->assertResponseSuccess($updateResp, 'update session timer'); - $this->assertEquals(7200, $updateResp->getData()->call->settings->limits->maxDurationSeconds); + self::assertSame(7200, $updateResp->getData()->call->settings->limits->maxDurationSeconds); // Reset to 0 $resetResp = $this->updateCall('default', $callID, new GeneratedModels\UpdateCallRequest( @@ -373,13 +314,15 @@ public function testCreateCallWithSessionTimer(): void ), )); $this->assertResponseSuccess($resetResp, 'reset session timer'); - $this->assertEquals(0, $resetResp->getData()->call->settings->limits->maxDurationSeconds); + self::assertSame(0, $resetResp->getData()->call->settings->limits->maxDurationSeconds); } /** * UserBlocking: Block/unblock user, verify blocked list. + * + * @test */ - public function testUserBlocking(): void + public function userBlocking(): void { $userIDs = $this->createTestUsers(2); @@ -394,9 +337,9 @@ public function testUserBlocking(): void $getResp = $this->client->getBlockedUsers($userIDs[0]); $this->assertResponseSuccess($getResp, 'get blocked users'); $blocks = $getResp->getData()->blocks; - $this->assertCount(1, $blocks); - $this->assertEquals($userIDs[0], $blocks[0]->userID); - $this->assertEquals($userIDs[1], $blocks[0]->blockedUserID); + self::assertCount(1, $blocks); + self::assertSame($userIDs[0], $blocks[0]->userID); + self::assertSame($userIDs[1], $blocks[0]->blockedUserID); // Unblock $unblockResp = $this->client->unblockUsers(new GeneratedModels\UnblockUsersRequest( @@ -408,13 +351,15 @@ public function testUserBlocking(): void // Verify empty $getResp2 = $this->client->getBlockedUsers($userIDs[0]); $this->assertResponseSuccess($getResp2, 'get blocked users after unblock'); - $this->assertEmpty($getResp2->getData()->blocks); + self::assertEmpty($getResp2->getData()->blocks); } /** * CreateCallWithBackstageAndJoinAhead: Create call with backstage + join_ahead_time. + * + * @test */ - public function testCreateCallWithBackstageAndJoinAhead(): void + public function createCallWithBackstageAndJoinAhead(): void { $userIDs = $this->createTestUsers(1); $callID = 'test-call-' . $this->randomString(10); @@ -435,7 +380,7 @@ public function testCreateCallWithBackstageAndJoinAhead(): void )); $this->assertResponseSuccess($response, 'create call with backstage'); $this->createdCalls[] = ['type' => 'default', 'id' => $callID]; - $this->assertEquals(300, $response->getData()->call->joinAheadTimeSeconds); + self::assertSame(300, $response->getData()->call->joinAheadTimeSeconds); // Update join ahead to 600 $updateResp = $this->updateCall('default', $callID, new GeneratedModels\UpdateCallRequest( @@ -446,7 +391,7 @@ public function testCreateCallWithBackstageAndJoinAhead(): void ), )); $this->assertResponseSuccess($updateResp, 'update join ahead time'); - $this->assertEquals(600, $updateResp->getData()->call->joinAheadTimeSeconds); + self::assertSame(600, $updateResp->getData()->call->joinAheadTimeSeconds); // Reset to 0 $resetResp = $this->updateCall('default', $callID, new GeneratedModels\UpdateCallRequest( @@ -457,13 +402,15 @@ public function testCreateCallWithBackstageAndJoinAhead(): void ), )); $this->assertResponseSuccess($resetResp, 'reset join ahead time'); - $this->assertEquals(0, $resetResp->getData()->call->joinAheadTimeSeconds); + self::assertSame(0, $resetResp->getData()->call->joinAheadTimeSeconds); } /** * DeleteCall (soft): Soft delete call, verify not found. + * + * @test */ - public function testDeleteCallSoft(): void + public function deleteCallSoft(): void { $userIDs = $this->createTestUsers(1); $callID = 'test-call-' . $this->randomString(10); @@ -473,25 +420,27 @@ public function testDeleteCallSoft(): void // Soft delete $delResp = $this->deleteCall('default', $callID, new GeneratedModels\DeleteCallRequest()); $this->assertResponseSuccess($delResp, 'soft delete call'); - $this->assertNotNull($delResp->getData()->call); - $this->assertNull($delResp->getData()->taskID); + self::assertNotNull($delResp->getData()->call); + self::assertNull($delResp->getData()->taskID); // Verify not found try { $this->getCall('default', $callID); - $this->fail('Expected error when getting soft-deleted call'); + self::fail('Expected error when getting soft-deleted call'); } catch (\Exception $e) { - $this->assertStringContainsString("Can't find call with id", $e->getMessage()); + self::assertStringContainsString("Can't find call with id", $e->getMessage()); } // Remove from tracked list since already deleted - $this->createdCalls = array_filter($this->createdCalls, fn($c) => $c['id'] !== $callID); + $this->createdCalls = array_filter($this->createdCalls, static fn ($c) => $c['id'] !== $callID); } /** * HardDeleteCall: Hard delete with task polling. + * + * @test */ - public function testHardDeleteCall(): void + public function hardDeleteCall(): void { $userIDs = $this->createTestUsers(1); $callID = 'test-call-' . $this->randomString(10); @@ -504,28 +453,30 @@ public function testHardDeleteCall(): void )); $this->assertResponseSuccess($delResp, 'hard delete call'); $taskID = $delResp->getData()->taskID; - $this->assertNotNull($taskID); + self::assertNotNull($taskID); // Wait for task completion $taskResult = $this->waitForTask($taskID); - $this->assertEquals('completed', $taskResult->status); + self::assertSame('completed', $taskResult->status); // Verify not found try { $this->getCall('default', $callID); - $this->fail('Expected error when getting hard-deleted call'); + self::fail('Expected error when getting hard-deleted call'); } catch (\Exception $e) { - $this->assertStringContainsString("Can't find call with id", $e->getMessage()); + self::assertStringContainsString("Can't find call with id", $e->getMessage()); } // Remove from tracked list since already deleted - $this->createdCalls = array_filter($this->createdCalls, fn($c) => $c['id'] !== $callID); + $this->createdCalls = array_filter($this->createdCalls, static fn ($c) => $c['id'] !== $callID); } /** * Teams: Create user with teams, create call with team, query. + * + * @test */ - public function testTeams(): void + public function teams(): void { $userID = 'test-user-' . uniqid(); $callID = 'test-call-' . $this->randomString(10); @@ -550,7 +501,7 @@ public function testTeams(): void )); $this->assertResponseSuccess($response, 'create call with team'); $this->createdCalls[] = ['type' => 'default', 'id' => $callID]; - $this->assertEquals('blue', $response->getData()->call->team); + self::assertSame('blue', $response->getData()->call->team); // Query users with team filter $usersResp = $this->client->queryUsers(new GeneratedModels\QueryUsersPayload( @@ -560,9 +511,9 @@ public function testTeams(): void ], )); $this->assertResponseSuccess($usersResp, 'query users with teams'); - $this->assertGreaterThan(0, count($usersResp->getData()->users)); - $foundIDs = array_map(fn($u) => $u->id, $usersResp->getData()->users); - $this->assertContains($userID, $foundIDs); + self::assertGreaterThan(0, count($usersResp->getData()->users)); + $foundIDs = array_map(static fn ($u) => $u->id, $usersResp->getData()->users); + self::assertContains($userID, $foundIDs); // Query calls with team filter $callsResp = $this->queryCalls(new GeneratedModels\QueryCallsRequest( @@ -572,13 +523,15 @@ public function testTeams(): void ], )); $this->assertResponseSuccess($callsResp, 'query calls with team'); - $this->assertGreaterThan(0, count($callsResp->getData()->calls)); + self::assertGreaterThan(0, count($callsResp->getData()->calls)); } /** * ExternalStorageOperations: Create/list/delete external storage. + * + * @test */ - public function testExternalStorageOperations(): void + public function externalStorageOperations(): void { $uniqueName = 'test-storage-' . $this->randomString(10); @@ -604,16 +557,17 @@ public function testExternalStorageOperations(): void $listResp = $this->client->listExternalStorage(); $this->assertResponseSuccess($listResp, 'list external storage'); $storages = $listResp->getData()->externalStorages; - $this->assertNotEmpty($storages); + self::assertNotEmpty($storages); $found = false; foreach ($storages as $storage) { if ($storage->name === $uniqueName) { $found = true; + break; } } - $this->assertTrue($found, 'Created external storage should be in the list'); + self::assertTrue($found, 'Created external storage should be in the list'); // Delete with retry for eventual consistency $this->retryUntilSuccess(function () use ($uniqueName) { @@ -622,13 +576,15 @@ public function testExternalStorageOperations(): void }, maxAttempts: 5, sleepMs: 2000); // Remove from tracked list - $this->createdExternalStorages = array_filter($this->createdExternalStorages, fn($n) => $n !== $uniqueName); + $this->createdExternalStorages = array_filter($this->createdExternalStorages, static fn ($n) => $n !== $uniqueName); } /** * EnableCallRecordingAndBackstageMode: Update call settings for recording and backstage. + * + * @test */ - public function testEnableCallRecordingAndBackstageMode(): void + public function enableCallRecordingAndBackstageMode(): void { $userIDs = $this->createTestUsers(1); $callID = 'test-call-' . $this->randomString(10); @@ -645,7 +601,7 @@ public function testEnableCallRecordingAndBackstageMode(): void ), )); $this->assertResponseSuccess($recResp, 'enable recording'); - $this->assertEquals('available', $recResp->getData()->call->settings->recording->mode); + self::assertSame('available', $recResp->getData()->call->settings->recording->mode); // Enable backstage $bsResp = $this->updateCall('default', $callID, new GeneratedModels\UpdateCallRequest( @@ -656,13 +612,15 @@ public function testEnableCallRecordingAndBackstageMode(): void ), )); $this->assertResponseSuccess($bsResp, 'enable backstage'); - $this->assertTrue($bsResp->getData()->call->settings->backstage->enabled); + self::assertTrue($bsResp->getData()->call->settings->backstage->enabled); } /** * DeleteRecordingsAndTranscriptions: Attempt to delete non-existent recording/transcription (error case). + * + * @test */ - public function testDeleteRecordingsAndTranscriptions(): void + public function deleteRecordingsAndTranscriptions(): void { $userIDs = $this->createTestUsers(1); $callID = 'test-call-' . $this->randomString(10); @@ -672,19 +630,97 @@ public function testDeleteRecordingsAndTranscriptions(): void // Delete non-existent recording should fail try { $this->deleteRecording('default', $callID, 'non-existent-session', 'non-existent-filename'); - $this->fail('Expected error when deleting non-existent recording'); + self::fail('Expected error when deleting non-existent recording'); } catch (\Exception $e) { // Expected - API returns error for non-existent recording - $this->assertNotEmpty($e->getMessage()); + self::assertNotEmpty($e->getMessage()); } // Delete non-existent transcription should fail try { $this->deleteTranscription('default', $callID, 'non-existent-session', 'non-existent-filename'); - $this->fail('Expected error when deleting non-existent transcription'); + self::fail('Expected error when deleting non-existent transcription'); } catch (\Exception $e) { // Expected - API returns error for non-existent transcription - $this->assertNotEmpty($e->getMessage()); + self::assertNotEmpty($e->getMessage()); } } + + /** + * Helper: create a unique call type and track for cleanup. + */ + private function createTrackedCallType(string $name): GeneratedModels\CreateCallTypeResponse + { + // Clean up excess call types first (same pattern as Go SDK) + try { + $listResp = $this->listCallTypesVideo(); + if ($listResp->isSuccessful() && $listResp->getData()->callTypes !== null && count($listResp->getData()->callTypes) > 10) { + $builtIn = ['default', 'livestream', 'audio_room', 'development']; + foreach ($listResp->getData()->callTypes as $ctName => $ct) { + if (!in_array($ctName, $builtIn, true)) { + try { + $this->deleteCallTypeVideo($ctName); + } catch (\Exception $e) { + // Ignore + } + } + } + } + } catch (\Exception $e) { + // Ignore list errors + } + + $response = $this->createCallTypeVideo(new GeneratedModels\CreateCallTypeRequest( + name: $name, + grants: [ + 'admin' => ['send-audio', 'send-video', 'mute-users'], + 'user' => ['send-audio', 'send-video'], + ], + settings: new GeneratedModels\CallSettingsRequest( + audio: new GeneratedModels\AudioSettingsRequest( + defaultDevice: 'speaker', + micDefaultOn: true, + ), + screensharing: new GeneratedModels\ScreensharingSettingsRequest( + accessRequestEnabled: false, + enabled: true, + ), + ), + notificationSettings: new GeneratedModels\NotificationSettingsRequest( + enabled: true, + callNotification: new GeneratedModels\EventNotificationSettingsRequest( + enabled: true, + apns: new GeneratedModels\APNSPayload( + title: '{{ user.display_name }} invites you to a call', + body: '', + ), + ), + sessionStarted: new GeneratedModels\EventNotificationSettingsRequest( + enabled: false, + apns: new GeneratedModels\APNSPayload( + title: '{{ user.display_name }} invites you to a call', + body: '', + ), + ), + callLiveStarted: new GeneratedModels\EventNotificationSettingsRequest( + enabled: false, + apns: new GeneratedModels\APNSPayload( + title: '{{ user.display_name }} invites you to a call', + body: '', + ), + ), + callRing: new GeneratedModels\EventNotificationSettingsRequest( + enabled: false, + apns: new GeneratedModels\APNSPayload( + title: '{{ user.display_name }} invites you to a call', + body: '', + ), + ), + ), + )); + $this->assertResponseSuccess($response, 'create call type'); + $this->createdCallTypes[] = $name; + + return $response->getData(); + } } diff --git a/tests/WebhookTest.php b/tests/WebhookTest.php index 97cd21e..21f7462 100644 --- a/tests/WebhookTest.php +++ b/tests/WebhookTest.php @@ -1,88 +1,115 @@ -computeSignature($this->body, $this->secret); - $this->assertTrue(Webhook::verifySignature($this->body, $signature, $this->secret)); + self::assertTrue(Webhook::verifySignature($this->body, $signature, $this->secret)); } - public function testVerifySignatureWrongSignature(): void + /** + * @test + */ + public function verifySignatureWrongSignature(): void { - $this->assertFalse(Webhook::verifySignature($this->body, 'invalidsignature', $this->secret)); + self::assertFalse(Webhook::verifySignature($this->body, 'invalidsignature', $this->secret)); } - public function testVerifySignatureTamperedBody(): void + /** + * @test + */ + public function verifySignatureTamperedBody(): void { $signature = $this->computeSignature($this->body, $this->secret); - $this->assertFalse(Webhook::verifySignature('{"type":"tampered"}', $signature, $this->secret)); + self::assertFalse(Webhook::verifySignature('{"type":"tampered"}', $signature, $this->secret)); } - public function testVerifySignatureWrongSecret(): void + /** + * @test + */ + public function verifySignatureWrongSecret(): void { $signature = $this->computeSignature($this->body, $this->secret); - $this->assertFalse(Webhook::verifySignature($this->body, $signature, 'wrong-secret')); + self::assertFalse(Webhook::verifySignature($this->body, $signature, 'wrong-secret')); } - public function testVerifySignatureEmptySignature(): void + /** + * @test + */ + public function verifySignatureEmptySignature(): void { - $this->assertFalse(Webhook::verifySignature($this->body, '', $this->secret)); + self::assertFalse(Webhook::verifySignature($this->body, '', $this->secret)); } - public function testGetEventTypeValid(): void + /** + * @test + */ + public function getEventTypeValid(): void { - $this->assertEquals('message.new', Webhook::getEventType('{"type":"message.new"}')); + self::assertSame('message.new', Webhook::getEventType('{"type":"message.new"}')); } - public function testGetEventTypeFromArray(): void + /** + * @test + */ + public function getEventTypeFromArray(): void { - $this->assertEquals('message.new', Webhook::getEventType(['type' => 'message.new'])); + self::assertSame('message.new', Webhook::getEventType(['type' => 'message.new'])); } - public function testGetEventTypeMissingField(): void + /** + * @test + */ + public function getEventTypeMissingField(): void { - $this->assertNull(Webhook::getEventType('{"foo":"bar"}')); + self::assertNull(Webhook::getEventType('{"foo":"bar"}')); } - public function testGetEventTypeInvalidJson(): void + /** + * @test + */ + public function getEventTypeInvalidJson(): void { $this->expectException(\InvalidArgumentException::class); Webhook::getEventType('not json'); } - public function testGetEventTypeEmptyObject(): void + /** + * @test + */ + public function getEventTypeEmptyObject(): void { - $this->assertNull(Webhook::getEventType('{}')); + self::assertNull(Webhook::getEventType('{}')); } /** - * @dataProvider webhookEventProvider + * @dataProvider provideParseWebhookEventCases + * + * @test */ - public function testParseWebhookEvent(string $eventType, string $expectedClassName): void + public function parseWebhookEvent(string $eventType, string $expectedClassName): void { $payload = '{"type":"' . $eventType . '"}'; $event = Webhook::parseWebhookEvent($payload); - $this->assertNotNull($event); + self::assertNotNull($event); $parts = explode('\\', get_class($event)); - $this->assertEquals($expectedClassName, end($parts)); + self::assertSame($expectedClassName, end($parts)); } - public static function webhookEventProvider(): array + public static function provideParseWebhookEventCases(): iterable { return [ '*' => ['*', 'CustomEvent'], @@ -254,24 +281,38 @@ public static function webhookEventProvider(): array ]; } - public function testParseWebhookEventUnknownType(): void + /** + * @test + */ + public function parseWebhookEventUnknownType(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Unknown webhook event type'); Webhook::parseWebhookEvent('{"type":"unknown.event"}'); } - public function testParseWebhookEventMissingType(): void + /** + * @test + */ + public function parseWebhookEventMissingType(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage("missing 'type' field"); Webhook::parseWebhookEvent('{"foo":"bar"}'); } - public function testParseWebhookEventInvalidJson(): void + /** + * @test + */ + public function parseWebhookEventInvalidJson(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid JSON'); Webhook::parseWebhookEvent('not json'); } + + private function computeSignature(string $body, string $secret): string + { + return hash_hmac('sha256', $body, $secret); + } }