From e9b95ceaada6d2b9c16ec40d93551f018ed3322b Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 11:21:59 +0100 Subject: [PATCH 01/29] feat: add 16 user integration tests for chat test parity Co-Authored-By: Claude Opus 4.6 --- spec/integration/chat_test_helpers.rb | 173 +++++++ .../integration/chat_user_integration_spec.rb | 483 ++++++++++++++++++ 2 files changed, 656 insertions(+) create mode 100644 spec/integration/chat_test_helpers.rb create mode 100644 spec/integration/chat_user_integration_spec.rb diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb new file mode 100644 index 0000000..3690d65 --- /dev/null +++ b/spec/integration/chat_test_helpers.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'securerandom' +require 'json' +require 'dotenv' +require_relative '../../lib/getstream_ruby' + +# Shared helpers for chat integration tests. +# Include this module in RSpec describe blocks and call `init_chat_client` +# in a before(:all) hook. +module ChatTestHelpers + # --------------------------------------------------------------------------- + # Setup / teardown + # --------------------------------------------------------------------------- + + def init_chat_client + Dotenv.load('.env') if File.exist?('.env') + @client = GetStreamRuby.client + @created_user_ids = [] + @created_channel_cids = [] + end + + def cleanup_chat_resources + # Delete channels first (they reference users) + @created_channel_cids&.each do |cid| + type, id = cid.split(':', 2) + @client.make_request( + :delete, + "/api/v2/chat/channels/#{type}/#{id}", + query_params: { 'hard_delete' => 'true' } + ) + rescue StandardError => e + puts "Warning: Failed to delete channel #{cid}: #{e.message}" + end + + # Delete users with retry + delete_users_with_retry(@created_user_ids) if @created_user_ids && !@created_user_ids.empty? + end + + # --------------------------------------------------------------------------- + # Helper 1: random_string + # --------------------------------------------------------------------------- + + def random_string(n = 8) + SecureRandom.alphanumeric(n) + end + + # --------------------------------------------------------------------------- + # Helper 2: create_test_users + # --------------------------------------------------------------------------- + + def create_test_users(n) + ids = Array.new(n) { "test-user-#{SecureRandom.uuid}" } + users = {} + ids.each do |id| + users[id] = GetStream::Generated::Models::UserRequest.new( + id: id, + name: "Test User #{id[0..7]}", + role: 'user' + ) + end + + response = @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new(users: users) + ) + @created_user_ids.concat(ids) + [ids, response] + end + + # --------------------------------------------------------------------------- + # Helper 3: create_test_channel + # --------------------------------------------------------------------------- + + def create_test_channel(creator_id) + channel_id = "test-ch-#{SecureRandom.hex(6)}" + body = { data: { created_by_id: creator_id } } + response = @client.make_request( + :post, + "/api/v2/chat/channels/messaging/#{channel_id}/query", + body: body + ) + @created_channel_cids << "messaging:#{channel_id}" + ['messaging', channel_id, response] + end + + # --------------------------------------------------------------------------- + # Helper 4: create_test_channel_with_members + # --------------------------------------------------------------------------- + + def create_test_channel_with_members(creator_id, member_ids) + channel_id = "test-ch-#{SecureRandom.hex(6)}" + members = member_ids.map { |id| { user_id: id } } + body = { data: { created_by_id: creator_id, members: members } } + response = @client.make_request( + :post, + "/api/v2/chat/channels/messaging/#{channel_id}/query", + body: body + ) + @created_channel_cids << "messaging:#{channel_id}" + ['messaging', channel_id, response] + end + + # --------------------------------------------------------------------------- + # Helper 5: send_test_message + # --------------------------------------------------------------------------- + + def send_test_message(channel_type, channel_id, user_id, text) + body = { message: { text: text, user_id: user_id } } + resp = @client.make_request( + :post, + "/api/v2/chat/channels/#{channel_type}/#{channel_id}/message", + body: body + ) + resp.message.id + end + + # --------------------------------------------------------------------------- + # Helper 6: delete_users_with_retry + # --------------------------------------------------------------------------- + + def delete_users_with_retry(user_ids) + 10.times do |i| + @client.common.delete_users( + GetStream::Generated::Models::DeleteUsersRequest.new( + user_ids: user_ids, + user: 'hard', + messages: 'hard', + conversations: 'hard' + ) + ) + return + rescue GetStreamRuby::APIError => e + return unless e.message.include?('Too many requests') + + sleep((i + 1) * 3) + end + end + + # --------------------------------------------------------------------------- + # Helper 7: wait_for_task + # --------------------------------------------------------------------------- + + def wait_for_task(task_id) + 30.times do + result = @client.common.get_task(task_id) + return result if %w[completed failed].include?(result.status) + + sleep(1) + end + raise "Task #{task_id} did not complete after 30 attempts" + end + + # --------------------------------------------------------------------------- + # Channel API wrappers (for tests that need direct channel operations) + # --------------------------------------------------------------------------- + + def get_or_create_channel(type, id, body = {}) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/query", body: body) + end + + def delete_channel(type, id, hard: false) + query_params = hard ? { 'hard_delete' => 'true' } : {} + @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}", query_params: query_params) + end + + def query_channels(body) + @client.make_request(:post, '/api/v2/chat/channels', body: body) + end + + def send_message(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/message", body: body) + end +end diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb new file mode 100644 index 0000000..6cac704 --- /dev/null +++ b/spec/integration/chat_user_integration_spec.rb @@ -0,0 +1,483 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat User Integration', type: :integration do + include ChatTestHelpers + + before(:all) do + init_chat_client + end + + after(:all) do + cleanup_chat_resources + end + + # Helper to query users with a filter + def query_users_with_filter(filter, **opts) + payload = { 'filter_conditions' => filter } + payload['limit'] = opts[:limit] if opts[:limit] + payload['offset'] = opts[:offset] if opts[:offset] + payload['include_deactivated_users'] = opts[:include_deactivated_users] if opts.key?(:include_deactivated_users) + payload['sort'] = opts[:sort] if opts[:sort] + @client.common.query_users(JSON.generate(payload)) + end + + describe 'UpsertUsers' do + it 'creates 2 users and verifies both in response' do + user_ids, response = create_test_users(2) + + expect(response).to be_a(GetStreamRuby::StreamResponse) + expect(user_ids.length).to eq(2) + + users_hash = response.users + expect(users_hash).not_to be_nil + user_ids.each do |uid| + expect(users_hash.to_h.key?(uid)).to be true + end + end + end + + describe 'QueryUsers' do + it 'queries users with $in filter and verifies found' do + user_ids, _resp = create_test_users(2) + + resp = query_users_with_filter({ 'id' => { '$in' => user_ids } }) + expect(resp.users).not_to be_nil + expect(resp.users.length).to be >= 2 + + returned_ids = resp.users.map { |u| u.to_h['id'] || u.id } + user_ids.each do |uid| + expect(returned_ids).to include(uid) + end + end + end + + describe 'QueryUsersWithOffsetLimit' do + it 'queries with offset=1 limit=2 and verifies exactly 2 returned' do + user_ids, _resp = create_test_users(3) + + resp = query_users_with_filter( + { 'id' => { '$in' => user_ids } }, + offset: 1, + limit: 2, + sort: [{ 'field' => 'id', 'direction' => 1 }] + ) + expect(resp.users).not_to be_nil + expect(resp.users.length).to eq(2) + end + end + + describe 'PartialUpdateUser' do + it 'sets custom fields then unsets one' do + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + # Set country and role + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + set: { 'country' => 'NL', 'role' => 'admin' } + ) + ] + ) + ) + + # Verify set + resp = query_users_with_filter({ 'id' => uid }) + user = resp.users.first + user_h = user.to_h + # Custom fields may be at top-level or under 'custom' + country = user_h['custom'].is_a?(Hash) ? user_h['custom']['country'] : user_h['country'] + expect(country).to eq('NL') + + # Unset country + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + unset: ['country'] + ) + ] + ) + ) + + # Verify unset + resp2 = query_users_with_filter({ 'id' => uid }) + user2 = resp2.users.first + user2_hash = user2.to_h + country2 = user2_hash['custom'].is_a?(Hash) ? user2_hash['custom']['country'] : user2_hash['country'] + expect(country2).to be_nil + end + end + + describe 'BlockUnblockUser' do + it 'blocks user, verifies in blocked list, unblocks, verifies removed' do + user_ids, _resp = create_test_users(2) + blocker_id = user_ids[0] + blocked_id = user_ids[1] + + # Block + @client.common.block_users( + GetStream::Generated::Models::BlockUsersRequest.new( + blocked_user_id: blocked_id, + user_id: blocker_id + ) + ) + + # Verify blocked + blocked_resp = @client.common.get_blocked_users(blocker_id) + expect(blocked_resp.blocks).not_to be_nil + blocked_user_ids = blocked_resp.blocks.map { |b| b.to_h['blocked_user_id'] || b.blocked_user_id } + expect(blocked_user_ids).to include(blocked_id) + + # Unblock + @client.common.unblock_users( + GetStream::Generated::Models::UnblockUsersRequest.new( + blocked_user_id: blocked_id, + user_id: blocker_id + ) + ) + + # Verify unblocked + blocked_resp2 = @client.common.get_blocked_users(blocker_id) + blocked_user_ids2 = (blocked_resp2.blocks || []).map { |b| b.to_h['blocked_user_id'] || b.blocked_user_id } + expect(blocked_user_ids2).not_to include(blocked_id) + end + end + + describe 'DeactivateReactivateUser' do + it 'deactivates then reactivates a user' do + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + # Deactivate + @client.common.deactivate_user( + uid, + GetStream::Generated::Models::DeactivateUserRequest.new + ) + + # Reactivate + @client.common.reactivate_user( + uid, + GetStream::Generated::Models::ReactivateUserRequest.new + ) + + # Verify active by querying + resp = query_users_with_filter({ 'id' => uid }) + expect(resp.users.length).to eq(1) + end + end + + describe 'DeleteUsers' do + it 'deletes 2 users with retry and polls task until completed' do + user_ids, _resp = create_test_users(2) + + # Remove from tracked list so cleanup doesn't double-delete + user_ids.each { |uid| @created_user_ids.delete(uid) } + + resp = nil + 10.times do |i| + resp = @client.common.delete_users( + GetStream::Generated::Models::DeleteUsersRequest.new( + user_ids: user_ids, + user: 'hard', + messages: 'hard', + conversations: 'hard' + ) + ) + break + rescue GetStreamRuby::APIError => e + raise unless e.message.include?('Too many requests') + + sleep((i + 1) * 3) + end + + expect(resp).not_to be_nil + task_id = resp.task_id + expect(task_id).not_to be_nil + + result = wait_for_task(task_id) + expect(result.status).to eq('completed') + end + end + + describe 'ExportUser' do + it 'exports a user and verifies response not nil' do + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + resp = @client.common.export_user(uid) + expect(resp).not_to be_nil + end + end + + describe 'CreateGuest' do + it 'creates guest and verifies access token' do + guest_id = "test-guest-#{SecureRandom.uuid}" + + resp = @client.common.create_guest( + GetStream::Generated::Models::CreateGuestRequest.new( + user: GetStream::Generated::Models::UserRequest.new( + id: guest_id, + name: 'Test Guest' + ) + ) + ) + + expect(resp.access_token).not_to be_nil + expect(resp.access_token).not_to be_empty + + # Clean up the guest user + @created_user_ids << guest_id + rescue GetStreamRuby::APIError => e + skip('Guest access not enabled') if e.message.downcase.include?('guest') + raise + end + end + + describe 'UpsertUsersWithRoleAndTeamsRole' do + it 'creates user with role=admin, teams, and teams_role' do + uid = "test-user-#{SecureRandom.uuid}" + @created_user_ids << uid + + @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + uid => GetStream::Generated::Models::UserRequest.new( + id: uid, + name: "Admin User #{uid[0..7]}", + role: 'admin', + teams: ['blue'], + teams_role: { 'blue' => 'admin' } + ) + } + ) + ) + + resp = query_users_with_filter({ 'id' => uid }) + user = resp.users.first + user_h = user.to_h + expect(user_h['role']).to eq('admin') + expect(user_h['teams']).to include('blue') + expect(user_h['teams_role']).to eq({ 'blue' => 'admin' }) + end + end + + describe 'PartialUpdateUserWithTeam' do + it 'partial updates to add teams and teams_role' do + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + set: { + 'teams' => ['blue'], + 'teams_role' => { 'blue' => 'admin' } + } + ) + ] + ) + ) + + resp = query_users_with_filter({ 'id' => uid }) + user = resp.users.first + user_h = user.to_h + expect(user_h['teams']).to include('blue') + expect(user_h['teams_role']).to eq({ 'blue' => 'admin' }) + end + end + + describe 'UpdatePrivacySettings' do + it 'sets typing_indicators disabled then sets both typing + read_receipts' do + uid = "test-user-#{SecureRandom.uuid}" + @created_user_ids << uid + + # Create user with typing_indicators disabled + @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + uid => GetStream::Generated::Models::UserRequest.new( + id: uid, + name: "Privacy User #{uid[0..7]}", + privacy_settings: GetStream::Generated::Models::PrivacySettingsResponse.new( + typing_indicators: GetStream::Generated::Models::TypingIndicatorsResponse.new(enabled: false) + ) + ) + } + ) + ) + + resp = query_users_with_filter({ 'id' => uid }) + user_h = resp.users.first.to_h + expect(user_h.dig('privacy_settings', 'typing_indicators', 'enabled')).to eq(false) + + # Update both typing_indicators and read_receipts + @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + uid => GetStream::Generated::Models::UserRequest.new( + id: uid, + privacy_settings: GetStream::Generated::Models::PrivacySettingsResponse.new( + typing_indicators: GetStream::Generated::Models::TypingIndicatorsResponse.new(enabled: true), + read_receipts: GetStream::Generated::Models::ReadReceiptsResponse.new(enabled: false) + ) + ) + } + ) + ) + + resp2 = query_users_with_filter({ 'id' => uid }) + user_h2 = resp2.users.first.to_h + expect(user_h2.dig('privacy_settings', 'typing_indicators', 'enabled')).to eq(true) + expect(user_h2.dig('privacy_settings', 'read_receipts', 'enabled')).to eq(false) + end + end + + describe 'PartialUpdatePrivacySettings' do + it 'partial updates privacy settings incrementally' do + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + # First: set typing_indicators.enabled = true + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + set: { + 'privacy_settings' => { + 'typing_indicators' => { 'enabled' => true } + } + } + ) + ] + ) + ) + + resp = query_users_with_filter({ 'id' => uid }) + user_h = resp.users.first.to_h + expect(user_h.dig('privacy_settings', 'typing_indicators', 'enabled')).to eq(true) + + # Second: set read_receipts.enabled = false + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + set: { + 'privacy_settings' => { + 'read_receipts' => { 'enabled' => false } + } + } + ) + ] + ) + ) + + resp2 = query_users_with_filter({ 'id' => uid }) + user_h2 = resp2.users.first.to_h + expect(user_h2.dig('privacy_settings', 'read_receipts', 'enabled')).to eq(false) + end + end + + describe 'QueryUsersWithDeactivated' do + it 'deactivates one user, queries without/with include_deactivated' do + user_ids, _resp = create_test_users(3) + deactivated_id = user_ids.first + + # Deactivate one user + @client.common.deactivate_user( + deactivated_id, + GetStream::Generated::Models::DeactivateUserRequest.new + ) + + # Query WITHOUT include_deactivated_users — expect 2 + resp = query_users_with_filter({ 'id' => { '$in' => user_ids } }) + expect(resp.users.length).to eq(2) + + # Query WITH include_deactivated_users — expect 3 + resp2 = query_users_with_filter( + { 'id' => { '$in' => user_ids } }, + include_deactivated_users: true + ) + expect(resp2.users.length).to eq(3) + + # Reactivate for cleanup + @client.common.reactivate_user( + deactivated_id, + GetStream::Generated::Models::ReactivateUserRequest.new + ) + end + end + + describe 'DeactivateUsersPlural' do + it 'deactivates multiple users at once via async task' do + user_ids, _resp = create_test_users(2) + + resp = @client.common.deactivate_users( + GetStream::Generated::Models::DeactivateUsersRequest.new( + user_ids: user_ids + ) + ) + + task_id = resp.task_id + expect(task_id).not_to be_nil + + result = wait_for_task(task_id) + expect(result.status).to eq('completed') + + # Verify deactivated users don't appear in default query + query_resp = query_users_with_filter({ 'id' => { '$in' => user_ids } }) + expect(query_resp.users.length).to eq(0) + + # Reactivate for cleanup + user_ids.each do |uid| + @client.common.reactivate_user(uid, GetStream::Generated::Models::ReactivateUserRequest.new) + end + end + end + + describe 'UserCustomData' do + it 'creates user with custom fields and verifies persistence' do + uid = "test-user-#{SecureRandom.uuid}" + @created_user_ids << uid + + custom_data = { + 'favorite_color' => 'blue', + 'age' => 30, + 'tags' => %w[vip early_adopter] + } + + resp = @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + uid => GetStream::Generated::Models::UserRequest.new( + id: uid, + name: "Custom User #{uid[0..7]}", + custom: custom_data + ) + } + ) + ) + + # Verify in upsert response + users_hash = resp.users.to_h + expect(users_hash).to have_key(uid) + + # Verify via query + query_resp = query_users_with_filter({ 'id' => uid }) + user_h = query_resp.users.first.to_h + expect(user_h['custom']['favorite_color'] || user_h['favorite_color']).to eq('blue') + end + end +end From 1e450b78beb04df1a4cc6f9d9472b4c0fc60f095 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 11:54:38 +0100 Subject: [PATCH 02/29] feat: add 27 message integration tests for chat test parity Co-Authored-By: Claude Opus 4.6 --- .../chat_channel_integration_spec.rb | 817 ++++++++++++++++++ .../chat_message_integration_spec.rb | 604 +++++++++++++ 2 files changed, 1421 insertions(+) create mode 100644 spec/integration/chat_channel_integration_spec.rb create mode 100644 spec/integration/chat_message_integration_spec.rb diff --git a/spec/integration/chat_channel_integration_spec.rb b/spec/integration/chat_channel_integration_spec.rb new file mode 100644 index 0000000..e7b3b1e --- /dev/null +++ b/spec/integration/chat_channel_integration_spec.rb @@ -0,0 +1,817 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require 'tempfile' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Channel Integration', type: :integration do + include ChatTestHelpers + + before(:all) do + init_chat_client + # Create shared test users for all subtests + @shared_user_ids, _resp = create_test_users(4) + @creator_id = @shared_user_ids[0] + @member_id1 = @shared_user_ids[1] + @member_id2 = @shared_user_ids[2] + @member_id3 = @shared_user_ids[3] + end + + after(:all) do + cleanup_chat_resources + end + + # --------------------------------------------------------------------------- + # Channel API wrappers (beyond what ChatTestHelpers provides) + # --------------------------------------------------------------------------- + + def update_channel(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}", body: body) + end + + def update_channel_partial(type, id, body) + @client.make_request(:patch, "/api/v2/chat/channels/#{type}/#{id}", body: body) + end + + def delete_channels_batch(body) + @client.make_request(:post, '/api/v2/chat/channels/delete', body: body) + end + + def hide_channel(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/hide", body: body) + end + + def show_channel(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/show", body: body) + end + + def truncate_channel(type, id, body = {}) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/truncate", body: body) + end + + def mark_read(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/read", body: body) + end + + def mark_unread(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/unread", body: body) + end + + def send_event(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/event", body: body) + end + + def mute_channel(body) + @client.make_request(:post, '/api/v2/chat/moderation/mute/channel', body: body) + end + + def unmute_channel(body) + @client.make_request(:post, '/api/v2/chat/moderation/unmute/channel', body: body) + end + + def update_member_partial(type, id, body) + user_id = body.delete(:user_id) || body.delete('user_id') + @client.make_request( + :patch, + "/api/v2/chat/channels/#{type}/#{id}/member", + query_params: { 'user_id' => user_id }, + body: body + ) + end + + def query_members_api(payload) + @client.make_request( + :get, + '/api/v2/chat/members', + query_params: { 'payload' => JSON.generate(payload) } + ) + end + + def upload_channel_file(type, id, file_upload_request) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/file", body: file_upload_request) + end + + def delete_channel_file(type, id, url) + @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}/file", query_params: { 'url' => url }) + end + + def upload_channel_image(type, id, image_upload_request) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/image", body: image_upload_request) + end + + def delete_channel_image(type, id, url) + @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}/image", query_params: { 'url' => url }) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'CreateChannelWithID' do + it 'creates channel and verifies via QueryChannels' do + _type, channel_id, _resp = create_test_channel(@creator_id) + + resp = query_channels( + filter_conditions: { 'id' => channel_id } + ) + expect(resp.channels).not_to be_nil + expect(resp.channels).not_to be_empty + ch = resp.channels.first.to_h + expect(ch.dig('channel', 'id')).to eq(channel_id) + expect(ch.dig('channel', 'type')).to eq('messaging') + end + end + + describe 'CreateChannelWithMembers' do + it 'creates channel with 3 members and verifies count' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, + [@creator_id, @member_id1, @member_id2] + ) + + resp = get_or_create_channel('messaging', channel_id) + expect(resp.members).not_to be_nil + expect(resp.members.length).to be >= 3 + end + end + + describe 'CreateDistinctChannel' do + it 'creates distinct channel and verifies same CID on second call' do + members = [ + { user_id: @creator_id }, + { user_id: @member_id1 } + ] + + resp = @client.make_request( + :post, + '/api/v2/chat/channels/messaging/query', + body: { + data: { + created_by_id: @creator_id, + members: members + } + } + ) + expect(resp.channel).not_to be_nil + cid1 = resp.channel.to_h['cid'] + + # Call again with same members — should return same channel + resp2 = @client.make_request( + :post, + '/api/v2/chat/channels/messaging/query', + body: { + data: { + created_by_id: @creator_id, + members: members + } + } + ) + cid2 = resp2.channel.to_h['cid'] + expect(cid1).to eq(cid2) + + # Cleanup: hard delete + ch_id = resp.channel.to_h['id'] + @created_channel_cids << "messaging:#{ch_id}" unless @created_channel_cids.include?("messaging:#{ch_id}") + end + end + + describe 'QueryChannels' do + it 'creates channel and queries by type+id' do + _type, channel_id, _resp = create_test_channel(@creator_id) + + resp = query_channels( + filter_conditions: { 'type' => 'messaging', 'id' => channel_id } + ) + expect(resp.channels).not_to be_nil + expect(resp.channels).not_to be_empty + expect(resp.channels.first.to_h.dig('channel', 'id')).to eq(channel_id) + end + end + + describe 'UpdateChannel' do + it 'updates with custom data and message, verifies custom field' do + _type, channel_id, _resp = create_test_channel(@creator_id) + + resp = update_channel('messaging', channel_id, + data: { custom: { color: 'blue' } }, + message: { text: 'Channel updated!', user_id: @creator_id }) + expect(resp.channel).not_to be_nil + ch = resp.channel.to_h + custom = ch['custom'] || {} + expect(custom['color']).to eq('blue') + end + end + + describe 'PartialUpdateChannel' do + it 'sets fields then unsets one' do + _type, channel_id, _resp = create_test_channel(@creator_id) + + # Set fields + resp = update_channel_partial('messaging', channel_id, + set: { 'color' => 'red', 'description' => 'A test channel' }) + expect(resp.channel).not_to be_nil + ch = resp.channel.to_h + custom = ch['custom'] || {} + expect(custom['color']).to eq('red') + + # Unset fields + resp2 = update_channel_partial('messaging', channel_id, unset: ['color']) + expect(resp2.channel).not_to be_nil + ch2 = resp2.channel.to_h + custom2 = ch2['custom'] || {} + expect(custom2).not_to have_key('color') + end + end + + describe 'DeleteChannel' do + it 'soft deletes channel and verifies response' do + channel_id = "test-del-#{SecureRandom.hex(6)}" + get_or_create_channel('messaging', channel_id, + data: { created_by_id: @creator_id }) + @created_channel_cids << "messaging:#{channel_id}" + + resp = delete_channel('messaging', channel_id) + expect(resp.channel).not_to be_nil + end + end + + describe 'HardDeleteChannels' do + it 'hard deletes 2 channels via batch and polls task' do + _type1, channel_id1, _resp1 = create_test_channel(@creator_id) + _type2, channel_id2, _resp2 = create_test_channel(@creator_id) + + cid1 = "messaging:#{channel_id1}" + cid2 = "messaging:#{channel_id2}" + + # Remove from tracked list since batch delete will handle it + @created_channel_cids.delete(cid1) + @created_channel_cids.delete(cid2) + + resp = delete_channels_batch(cids: [cid1, cid2], hard_delete: true) + expect(resp.task_id).not_to be_nil + + result = wait_for_task(resp.task_id) + expect(result.status).to eq('completed') + end + end + + describe 'AddRemoveMembers' do + it 'adds 2 members, verifies count; removes 1, verifies removed' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + # Add members + update_channel('messaging', channel_id, + add_members: [{ user_id: @member_id2 }, { user_id: @member_id3 }]) + + # Verify members added + resp = get_or_create_channel('messaging', channel_id) + expect(resp.members.length).to be >= 4 + + # Remove a member + update_channel('messaging', channel_id, remove_members: [@member_id3]) + + # Verify member removed + resp2 = get_or_create_channel('messaging', channel_id) + member_ids = resp2.members.map { |m| m.to_h['user_id'] || m.to_h.dig('user', 'id') } + expect(member_ids).not_to include(@member_id3) + end + end + + describe 'QueryMembers' do + it 'creates channel with 3 members and queries members' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1, @member_id2] + ) + + resp = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: {} + ) + expect(resp.members).not_to be_nil + expect(resp.members.length).to be >= 3 + end + end + + describe 'InviteAcceptReject' do + it 'creates channel with invites, accepts one, rejects one' do + channel_id = "test-inv-#{SecureRandom.hex(6)}" + + get_or_create_channel('messaging', channel_id, + data: { + created_by_id: @creator_id, + members: [{ user_id: @creator_id }], + invites: [{ user_id: @member_id1 }, { user_id: @member_id2 }] + }) + @created_channel_cids << "messaging:#{channel_id}" + + # Accept invite + update_channel('messaging', channel_id, + accept_invite: true, + user_id: @member_id1) + + # Reject invite + update_channel('messaging', channel_id, + reject_invite: true, + user_id: @member_id2) + end + end + + describe 'HideShowChannel' do + it 'hides channel for user, then shows' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + # Hide + hide_channel('messaging', channel_id, user_id: @member_id1) + + # Show + show_channel('messaging', channel_id, user_id: @member_id1) + end + end + + describe 'TruncateChannel' do + it 'sends 3 messages, truncates, verifies empty' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + send_test_message('messaging', channel_id, @creator_id, 'Message 1') + send_test_message('messaging', channel_id, @creator_id, 'Message 2') + send_test_message('messaging', channel_id, @creator_id, 'Message 3') + + truncate_channel('messaging', channel_id) + + resp = get_or_create_channel('messaging', channel_id) + messages = resp.messages || [] + expect(messages).to be_empty + end + end + + describe 'FreezeUnfreezeChannel' do + it 'sets frozen=true, verifies; sets frozen=false, verifies' do + _type, channel_id, _resp = create_test_channel(@creator_id) + + # Freeze + resp = update_channel_partial('messaging', channel_id, set: { 'frozen' => true }) + expect(resp.channel.to_h['frozen']).to eq(true) + + # Unfreeze + resp2 = update_channel_partial('messaging', channel_id, set: { 'frozen' => false }) + expect(resp2.channel.to_h['frozen']).to eq(false) + end + end + + describe 'MarkReadUnread' do + it 'sends message, marks read, marks unread' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + msg_id = send_test_message('messaging', channel_id, @creator_id, 'Message to mark read') + + # Mark read + mark_read('messaging', channel_id, user_id: @member_id1) + + # Mark unread from this message + mark_unread('messaging', channel_id, user_id: @member_id1, message_id: msg_id) + end + end + + describe 'MuteUnmuteChannel' do + it 'mutes channel, verifies via query with muted=true; unmutes' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + cid = "messaging:#{channel_id}" + + # Mute + mute_resp = mute_channel(channel_cids: [cid], user_id: @member_id1) + expect(mute_resp).not_to be_nil + expect(mute_resp.channel_mute).not_to be_nil + expect(mute_resp.channel_mute.to_h.dig('channel', 'cid')).to eq(cid) + + # Verify via QueryChannels with muted=true + q_resp = query_channels( + filter_conditions: { 'muted' => true, 'cid' => cid }, + user_id: @member_id1 + ) + expect(q_resp.channels.length).to eq(1) + expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) + + # Unmute + unmute_channel(channel_cids: [cid], user_id: @member_id1) + + # Verify unmuted + q_resp2 = query_channels( + filter_conditions: { 'muted' => false, 'cid' => cid }, + user_id: @member_id1 + ) + expect(q_resp2.channels.length).to eq(1) + end + end + + describe 'MemberPartialUpdate' do + it 'sets custom fields on member; unsets one' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + # Set custom fields + resp = update_member_partial('messaging', channel_id, + user_id: @member_id1, + set: { 'role_label' => 'moderator', 'score' => 42 }) + expect(resp.channel_member).not_to be_nil + member_h = resp.channel_member.to_h + custom = member_h['custom'] || {} + expect(custom['role_label']).to eq('moderator') + + # Unset a custom field + resp2 = update_member_partial('messaging', channel_id, + user_id: @member_id1, + unset: ['score']) + expect(resp2.channel_member).not_to be_nil + member_h2 = resp2.channel_member.to_h + custom2 = member_h2['custom'] || {} + expect(custom2).not_to have_key('score') + end + end + + describe 'AssignRoles' do + it 'assigns channel_moderator role, verifies via QueryMembers' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + # Assign role + update_channel('messaging', channel_id, + assign_roles: [{ user_id: @member_id1, channel_role: 'channel_moderator' }]) + + # Verify via QueryMembers + q_resp = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: { 'id' => @member_id1 } + ) + expect(q_resp.members).not_to be_empty + expect(q_resp.members.first.to_h['channel_role']).to eq('channel_moderator') + end + end + + describe 'AddDemoteModerators' do + it 'adds moderator, verifies; demotes, verifies back to member' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + # Add moderator + update_channel('messaging', channel_id, add_moderators: [@member_id1]) + + # Verify role + q_resp = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: { 'id' => @member_id1 } + ) + expect(q_resp.members).not_to be_empty + expect(q_resp.members.first.to_h['channel_role']).to eq('channel_moderator') + + # Demote + update_channel('messaging', channel_id, demote_moderators: [@member_id1]) + + # Verify back to member + q_resp2 = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: { 'id' => @member_id1 } + ) + expect(q_resp2.members).not_to be_empty + expect(q_resp2.members.first.to_h['channel_role']).to eq('channel_member') + end + end + + describe 'MarkUnreadWithThread' do + it 'creates thread and marks unread from thread' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + # Send parent message + parent_id = send_test_message('messaging', channel_id, @creator_id, 'Parent for mark unread thread') + + # Send reply to create a thread + send_message('messaging', channel_id, + message: { text: 'Reply in thread', user_id: @creator_id, parent_id: parent_id }) + + # Mark unread from thread + mark_unread('messaging', channel_id, + user_id: @member_id1, + thread_id: parent_id) + end + end + + describe 'TruncateWithOptions' do + it 'truncates with message, skip_push, hard_delete' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + send_test_message('messaging', channel_id, @creator_id, 'Truncate msg 1') + send_test_message('messaging', channel_id, @creator_id, 'Truncate msg 2') + + truncate_channel('messaging', channel_id, + message: { text: 'Channel was truncated', user_id: @creator_id }, + skip_push: true, + hard_delete: true) + end + end + + describe 'PinUnpinChannel' do + it 'pins channel, verifies via query; unpins, verifies' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + cid = "messaging:#{channel_id}" + + # Pin + update_member_partial('messaging', channel_id, + user_id: @member_id1, + set: { 'pinned' => true }) + + # Verify pinned + q_resp = query_channels( + filter_conditions: { 'pinned' => true, 'cid' => cid }, + user_id: @member_id1 + ) + expect(q_resp.channels.length).to eq(1) + expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) + + # Unpin + update_member_partial('messaging', channel_id, + user_id: @member_id1, + set: { 'pinned' => false }) + + # Verify unpinned + q_resp2 = query_channels( + filter_conditions: { 'pinned' => false, 'cid' => cid }, + user_id: @member_id1 + ) + expect(q_resp2.channels.length).to eq(1) + end + end + + describe 'ArchiveUnarchiveChannel' do + it 'archives channel, verifies via query; unarchives, verifies' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + cid = "messaging:#{channel_id}" + + # Archive + update_member_partial('messaging', channel_id, + user_id: @member_id1, + set: { 'archived' => true }) + + # Verify archived + q_resp = query_channels( + filter_conditions: { 'archived' => true, 'cid' => cid }, + user_id: @member_id1 + ) + expect(q_resp.channels.length).to eq(1) + expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) + + # Unarchive + update_member_partial('messaging', channel_id, + user_id: @member_id1, + set: { 'archived' => false }) + + # Verify unarchived + q_resp2 = query_channels( + filter_conditions: { 'archived' => false, 'cid' => cid }, + user_id: @member_id1 + ) + expect(q_resp2.channels.length).to eq(1) + end + end + + describe 'AddMembersWithRoles' do + it 'adds members with specific channel roles, verifies' do + _type, channel_id, _resp = create_test_channel(@creator_id) + + new_user_ids, _resp = create_test_users(2) + mod_user_id = new_user_ids[0] + member_user_id = new_user_ids[1] + + # Add members with specific roles + update_channel('messaging', channel_id, + add_members: [ + { user_id: mod_user_id, channel_role: 'channel_moderator' }, + { user_id: member_user_id, channel_role: 'channel_member' } + ]) + + # Query to verify roles + q_resp = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: { 'id' => { '$in' => new_user_ids } } + ) + + role_map = {} + q_resp.members.each do |m| + mh = m.to_h + uid = mh['user_id'] || mh.dig('user', 'id') + role_map[uid] = mh['channel_role'] + end + + expect(role_map[mod_user_id]).to eq('channel_moderator') + expect(role_map[member_user_id]).to eq('channel_member') + end + end + + describe 'MessageCount' do + it 'sends message, queries channel, verifies message_count >= 1' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + send_test_message('messaging', channel_id, @creator_id, 'hello world') + + q_resp = query_channels( + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + user_id: @creator_id + ) + expect(q_resp.channels.length).to eq(1) + + channel_h = q_resp.channels.first.to_h.dig('channel') || {} + msg_count = channel_h['message_count'] + # message_count may be nil if disabled on channel type + expect(msg_count).to be >= 1 if msg_count + end + end + + describe 'SendChannelEvent' do + it 'sends typing.start event' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + send_event('messaging', channel_id, + event: { type: 'typing.start', user_id: @creator_id }) + end + end + + describe 'FilterTags' do + it 'adds filter tags, removes filter tag' do + _type, channel_id, _resp = create_test_channel(@creator_id) + + # Add filter tags + update_channel('messaging', channel_id, + add_filter_tags: %w[sports news]) + + # Verify tags were added + resp = get_or_create_channel('messaging', channel_id) + expect(resp.channel).not_to be_nil + + # Remove a filter tag + update_channel('messaging', channel_id, + remove_filter_tags: ['sports']) + end + end + + describe 'MessageCountDisabled' do + it 'disables count_messages via config_overrides, verifies message_count nil' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + # Disable count_messages + update_channel_partial('messaging', channel_id, + set: { + 'config_overrides' => { 'count_messages' => false } + }) + + send_test_message('messaging', channel_id, @creator_id, 'hello world disabled count') + + q_resp = query_channels( + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + user_id: @creator_id + ) + expect(q_resp.channels.length).to eq(1) + + channel_h = q_resp.channels.first.to_h.dig('channel') || {} + expect(channel_h['message_count']).to be_nil + end + end + + describe 'MarkUnreadWithTimestamp' do + it 'sends message, gets timestamp, marks unread from timestamp' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id1] + ) + + # Send message to get a valid timestamp + resp = send_message('messaging', channel_id, + message: { text: 'test message for timestamp unread', user_id: @creator_id }) + created_at = resp.message.to_h['created_at'] + expect(created_at).not_to be_nil + + # API may return created_at as nanosecond epoch integer; convert to RFC 3339 string + ts = if created_at.is_a?(Numeric) + Time.at(0, created_at, :nanosecond).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') + else + created_at.to_s + end + + # Mark unread from timestamp + mark_unread('messaging', channel_id, + user_id: @member_id1, + message_timestamp: ts) + end + end + + describe 'HideForCreator' do + it 'creates channel with hide_for_creator=true, verifies hidden' do + channel_id = "test-hide-#{SecureRandom.hex(6)}" + + get_or_create_channel('messaging', channel_id, + hide_for_creator: true, + data: { + created_by_id: @creator_id, + members: [ + { user_id: @creator_id }, + { user_id: @member_id1 } + ] + }) + @created_channel_cids << "messaging:#{channel_id}" + + # Channel should be hidden for creator + q_resp = query_channels( + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + user_id: @creator_id + ) + expect(q_resp.channels).to be_empty + end + end + + describe 'UploadAndDeleteFile' do + it 'uploads a text file, verifies URL, deletes file' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id] + ) + + # Create a temp file + tmpfile = Tempfile.new(['chat-test-', '.txt']) + tmpfile.write('hello world test file content') + tmpfile.close + + begin + upload_resp = upload_channel_file( + 'messaging', channel_id, + GetStream::Generated::Models::FileUploadRequest.new( + file: tmpfile.path, + user: GetStream::Generated::Models::OnlyUserID.new(id: @creator_id) + ) + ) + expect(upload_resp.file).not_to be_nil + file_url = upload_resp.file + expect(file_url).to include('http') + + # Delete file + delete_channel_file('messaging', channel_id, file_url) + ensure + tmpfile.unlink + end + end + end + + describe 'UploadAndDeleteImage' do + it 'uploads an image file, verifies URL, deletes image' do + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id] + ) + + # Use existing test image + image_path = File.join(__dir__, 'upload-test.png') + skip('upload-test.png not found') unless File.exist?(image_path) + + upload_resp = upload_channel_image( + 'messaging', channel_id, + GetStream::Generated::Models::ImageUploadRequest.new( + file: image_path, + user: GetStream::Generated::Models::OnlyUserID.new(id: @creator_id) + ) + ) + expect(upload_resp.file).not_to be_nil + image_url = upload_resp.file + expect(image_url).to include('http') + + # Delete image + delete_channel_image('messaging', channel_id, image_url) + end + end +end diff --git a/spec/integration/chat_message_integration_spec.rb b/spec/integration/chat_message_integration_spec.rb new file mode 100644 index 0000000..ec3152d --- /dev/null +++ b/spec/integration/chat_message_integration_spec.rb @@ -0,0 +1,604 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Message Integration', type: :integration do + include ChatTestHelpers + + before(:all) do + init_chat_client + # Create shared test users for all subtests + @shared_user_ids, _resp = create_test_users(3) + @user1 = @shared_user_ids[0] + @user2 = @shared_user_ids[1] + @user3 = @shared_user_ids[2] + end + + after(:all) do + cleanup_chat_resources + end + + # --------------------------------------------------------------------------- + # Message API wrappers + # --------------------------------------------------------------------------- + + def get_message(message_id) + @client.make_request(:get, "/api/v2/chat/messages/#{message_id}") + end + + def get_many_messages(type, id, message_ids) + @client.make_request( + :get, + "/api/v2/chat/channels/#{type}/#{id}/messages", + query_params: { 'ids' => message_ids.join(',') } + ) + end + + def update_message(message_id, body) + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}", body: body) + end + + def update_message_partial(message_id, body) + @client.make_request(:put, "/api/v2/chat/messages/#{message_id}", body: body) + end + + def delete_message(message_id, query_params = {}) + @client.make_request(:delete, "/api/v2/chat/messages/#{message_id}", query_params: query_params) + end + + def send_msg(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/message", body: body) + end + + def translate_message(message_id, body) + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/translate", body: body) + end + + def get_replies(parent_id, **query_params) + @client.make_request(:get, "/api/v2/chat/messages/#{parent_id}/replies", query_params: query_params) + end + + def search_messages(body) + @client.make_request(:get, '/api/v2/chat/search', query_params: { 'payload' => JSON.generate(body) }) + end + + def commit_message(message_id) + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/commit") + end + + def query_message_history(body) + @client.make_request(:post, '/api/v2/chat/messages/history', body: body) + end + + def hide_channel(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/hide", body: body) + end + + def undelete_message(message_id, body) + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/undelete", body: body) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'SendAndGetMessage' do + it 'sends message, gets by ID, verifies text' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + msg_text = "Hello from integration test #{SecureRandom.hex(8)}" + send_resp = send_msg('messaging', channel_id, + message: { text: msg_text, user_id: @user1 }) + expect(send_resp.message).not_to be_nil + msg_id = send_resp.message.id + expect(msg_id).not_to be_nil + expect(send_resp.message.to_h['text']).to eq(msg_text) + + # Get message by ID + get_resp = get_message(msg_id) + expect(get_resp.message).not_to be_nil + expect(get_resp.message.to_h['id']).to eq(msg_id) + expect(get_resp.message.to_h['text']).to eq(msg_text) + end + end + + describe 'GetManyMessages' do + it 'sends 3 messages, gets all 3 by IDs' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + id1 = send_test_message('messaging', channel_id, @user1, 'Msg 1') + id2 = send_test_message('messaging', channel_id, @user1, 'Msg 2') + id3 = send_test_message('messaging', channel_id, @user1, 'Msg 3') + + resp = get_many_messages('messaging', channel_id, [id1, id2, id3]) + expect(resp.messages).not_to be_nil + expect(resp.messages.length).to eq(3) + end + end + + describe 'UpdateMessage' do + it 'sends message, updates text, verifies' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, 'Original text') + + updated_text = "Updated text #{SecureRandom.hex(8)}" + resp = update_message(msg_id, message: { text: updated_text, user_id: @user1 }) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['text']).to eq(updated_text) + end + end + + describe 'PartialUpdateMessage' do + it 'sets custom fields; unsets one' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, 'Partial update test') + + # Set custom fields + resp = update_message_partial(msg_id, + set: { 'priority' => 'high', 'status' => 'reviewed' }, + user_id: @user1) + expect(resp.message).not_to be_nil + + # Unset custom field + resp2 = update_message_partial(msg_id, + unset: ['status'], + user_id: @user1) + expect(resp2.message).not_to be_nil + end + end + + describe 'DeleteMessage' do + it 'soft deletes, verifies type=deleted' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, 'Message to delete') + + resp = delete_message(msg_id) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['type']).to eq('deleted') + end + end + + describe 'HardDeleteMessage' do + it 'hard deletes, verifies type=deleted' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, 'Message to hard delete') + + resp = delete_message(msg_id, { 'hard' => 'true' }) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['type']).to eq('deleted') + end + end + + describe 'PinUnpinMessage' do + it 'sends pinned message; unpins via partial update' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + # Send a pinned message + send_resp = send_msg('messaging', channel_id, + message: { text: 'Pinned message', user_id: @user1, pinned: true }) + expect(send_resp.message).not_to be_nil + msg_id = send_resp.message.id + expect(send_resp.message.to_h['pinned']).to eq(true) + + # Unpin via partial update + resp = update_message_partial(msg_id, + set: { 'pinned' => false }, + user_id: @user1) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['pinned']).to eq(false) + end + end + + describe 'TranslateMessage' do + it 'translates to Spanish, verifies i18n field' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, 'Hello, how are you?') + + resp = translate_message(msg_id, language: 'es') + expect(resp.message).not_to be_nil + i18n = resp.message.to_h['i18n'] + expect(i18n).not_to be_nil + end + end + + describe 'ThreadReply' do + it 'sends parent, sends reply with parent_id, gets replies' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + # Send parent message + parent_id = send_test_message('messaging', channel_id, @user1, 'Parent message for thread') + + # Send reply + reply_resp = send_msg('messaging', channel_id, + message: { text: 'Reply to parent', user_id: @user2, parent_id: parent_id }) + expect(reply_resp.message).not_to be_nil + expect(reply_resp.message.id).not_to be_nil + + # Get replies + replies_resp = get_replies(parent_id) + expect(replies_resp.messages).not_to be_nil + expect(replies_resp.messages.length).to be >= 1 + end + end + + describe 'SearchMessages' do + it 'sends message with unique term, waits, searches, verifies found' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + search_term = "uniquesearch#{SecureRandom.hex(8)}" + send_test_message('messaging', channel_id, @user1, "This message contains #{search_term} for testing") + + # Wait for indexing + sleep(2) + + resp = search_messages( + query: search_term, + filter_conditions: { 'cid' => "messaging:#{channel_id}" } + ) + expect(resp.results).not_to be_nil + expect(resp.results).not_to be_empty + end + end + + describe 'SilentMessage' do + it 'sends with silent=true, verifies' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + resp = send_msg('messaging', channel_id, + message: { text: 'This is a silent message', user_id: @user1, silent: true }) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['silent']).to eq(true) + end + end + + describe 'PendingMessage' do + it 'sends pending, commits, verifies (skip if not enabled)' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + begin + send_resp = send_msg('messaging', channel_id, + message: { text: 'Pending message text', user_id: @user1 }, + pending: true, + skip_push: true) + rescue StandardError => e + if e.message.include?('pending messages not enabled') || e.message.include?('feature flag') + skip('Pending messages feature not enabled for this app') + end + raise + end + + expect(send_resp.message).not_to be_nil + msg_id = send_resp.message.id + expect(msg_id).not_to be_nil + + # Commit the pending message + commit_resp = commit_message(msg_id) + expect(commit_resp.message).not_to be_nil + expect(commit_resp.message.to_h['id']).to eq(msg_id) + end + end + + describe 'QueryMessageHistory' do + it 'sends, updates twice, queries history, verifies entries (skip if not enabled)' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + # Send initial message + send_resp = send_msg('messaging', channel_id, + message: { text: 'initial text', user_id: @user1, + custom: { 'custom_field' => 'custom value' } }) + msg_id = send_resp.message.id + + # Update by user1 + update_message(msg_id, message: { text: 'updated text', user_id: @user1, + custom: { 'custom_field' => 'updated custom value' } }) + + # Update by user2 + update_message(msg_id, message: { text: 'updated text 2', user_id: @user2 }) + + # Query message history + begin + hist_resp = query_message_history( + filter: { 'message_id' => msg_id }, + sort: [] + ) + rescue StandardError => e + if e.message.include?('feature flag') || e.message.include?('not enabled') + skip('QueryMessageHistory feature not enabled for this app') + end + raise + end + + expect(hist_resp.message_history).not_to be_nil + expect(hist_resp.message_history.length).to be >= 2 + + # Verify history entries reference the correct message + hist_resp.message_history.each do |entry| + h = entry.to_h + expect(h['message_id']).to eq(msg_id) + end + + # Verify text values (descending by default: most recent first) + expect(hist_resp.message_history[0].to_h['text']).to eq('updated text') + expect(hist_resp.message_history[1].to_h['text']).to eq('initial text') + end + end + + describe 'QueryMessageHistorySort' do + it 'queries history with ascending sort' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + send_resp = send_msg('messaging', channel_id, + message: { text: 'sort initial', user_id: @user1 }) + msg_id = send_resp.message.id + + update_message(msg_id, message: { text: 'sort updated 1', user_id: @user1 }) + update_message(msg_id, message: { text: 'sort updated 2', user_id: @user1 }) + + begin + hist_resp = query_message_history( + filter: { 'message_id' => msg_id }, + sort: [{ 'field' => 'message_updated_at', 'direction' => 1 }] + ) + rescue StandardError => e + if e.message.include?('feature flag') || e.message.include?('not enabled') + skip('QueryMessageHistory feature not enabled for this app') + end + raise + end + + expect(hist_resp.message_history).not_to be_nil + expect(hist_resp.message_history.length).to be >= 2 + + # Ascending: oldest first + expect(hist_resp.message_history[0].to_h['text']).to eq('sort initial') + end + end + + describe 'SkipEnrichUrl' do + it 'sends with URL and skip_enrich_url=true, verifies no attachments' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + send_resp = send_msg('messaging', channel_id, + message: { text: 'Check out https://getstream.io for more info', user_id: @user1 }, + skip_enrich_url: true) + expect(send_resp.message).not_to be_nil + attachments = send_resp.message.to_h['attachments'] || [] + expect(attachments).to be_empty + + # Verify via GetMessage that attachments remain empty + sleep(3) + get_resp = get_message(send_resp.message.id) + attachments2 = get_resp.message.to_h['attachments'] || [] + expect(attachments2).to be_empty + end + end + + describe 'KeepChannelHidden' do + it 'hides channel, sends with keep_channel_hidden=true, verifies still hidden' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + cid = "messaging:#{channel_id}" + + # Hide the channel + hide_channel('messaging', channel_id, user_id: @user1) + + # Send a message with keep_channel_hidden=true + send_msg('messaging', channel_id, + message: { text: 'Hidden message', user_id: @user1 }, + keep_channel_hidden: true) + + # Query channels — the channel should still be hidden + q_resp = query_channels( + filter_conditions: { 'cid' => cid }, + user_id: @user1 + ) + expect(q_resp.channels).to be_empty + end + end + + describe 'UndeleteMessage' do + it 'soft deletes, undeletes, verifies restored' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, 'Message to undelete') + + # Soft delete + delete_message(msg_id) + + # Verify deleted + get_resp = get_message(msg_id) + expect(get_resp.message.to_h['type']).to eq('deleted') + + # Undelete + begin + undel_resp = undelete_message(msg_id, undeleted_by: @user1) + rescue StandardError => e + if e.message.include?('undeleted_by') || e.message.include?('required field') + skip('UndeleteMessage requires undeleted_by field not yet in generated request struct') + end + raise + end + expect(undel_resp.message).not_to be_nil + expect(undel_resp.message.to_h['type']).not_to eq('deleted') + expect(undel_resp.message.to_h['text']).to eq('Message to undelete') + end + end + + describe 'RestrictedVisibility' do + it 'sends with restricted_visibility list (skip if not enabled)' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + begin + send_resp = send_msg('messaging', channel_id, + message: { text: 'Secret message', user_id: @user1, + restricted_visibility: [@user1] }) + rescue StandardError => e + if e.message.include?('private messaging is not allowed') || e.message.include?('not enabled') + skip('RestrictedVisibility (private messaging) is not enabled for this app') + end + raise + end + + expect(send_resp.message.to_h['restricted_visibility']).to eq([@user1]) + end + end + + describe 'DeleteMessageForMe' do + it 'deletes message with delete_for_me=true' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, 'test message to delete for me') + + delete_message(msg_id, { 'delete_for_me' => 'true', 'deleted_by' => @user1 }) + end + end + + describe 'PinExpiration' do + it 'pins with 3s expiry, waits 4s, verifies expired' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + msg_id = send_test_message('messaging', channel_id, @user2, 'Message to pin with expiry') + + # Pin with 3 second expiration + expiry = (Time.now.utc + 3).strftime('%Y-%m-%dT%H:%M:%S.%6NZ') + pin_resp = update_message_partial(msg_id, + set: { 'pinned' => true, 'pin_expires' => expiry }, + user_id: @user1) + expect(pin_resp.message).not_to be_nil + expect(pin_resp.message.to_h['pinned']).to eq(true) + + # Wait for pin to expire + sleep(4) + + # Verify pin expired + get_resp = get_message(msg_id) + expect(get_resp.message.to_h['pinned']).to eq(false) + end + end + + describe 'SystemMessage' do + it 'sends with type=system, verifies' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + resp = send_msg('messaging', channel_id, + message: { text: 'User joined the channel', user_id: @user1, type: 'system' }) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['type']).to eq('system') + end + end + + describe 'PendingFalse' do + it 'sends with pending=false, verifies immediately available' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + send_resp = send_msg('messaging', channel_id, + message: { text: 'Non-pending message', user_id: @user1 }, + pending: false) + expect(send_resp.message).not_to be_nil + + # Get the message to verify it's immediately available + get_resp = get_message(send_resp.message.id) + expect(get_resp.message.to_h['text']).to eq('Non-pending message') + end + end + + describe 'SearchWithMessageFilters' do + it 'searches using message_filter_conditions' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + search_term = "filterable#{SecureRandom.hex(8)}" + send_test_message('messaging', channel_id, @user1, "This has #{search_term} text") + send_test_message('messaging', channel_id, @user1, "This also has #{search_term} text") + + # Wait for indexing + sleep(2) + + resp = search_messages( + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + message_filter_conditions: { 'text' => { '$q' => search_term } } + ) + expect(resp.results).not_to be_nil + expect(resp.results.length).to be >= 2 + end + end + + describe 'SearchQueryAndMessageFiltersError' do + it 'verifies error when using both query and message_filter_conditions' do + expect do + search_messages( + filter_conditions: { 'members' => { '$in' => [@user1] } }, + query: 'test', + message_filter_conditions: { 'text' => { '$q' => 'test' } } + ) + end.to raise_error(StandardError) + end + end + + describe 'SearchOffsetAndSortError' do + it 'verifies error when using offset with sort' do + # The API may or may not reject offset+sort. Verify either an error or a valid response. + begin + resp = search_messages( + filter_conditions: { 'members' => { '$in' => [@user1] } }, + query: 'test', + offset: 1, + sort: [{ 'field' => 'created_at', 'direction' => -1 }] + ) + # If no error, the API accepts the combination — verify a valid response + expect(resp).not_to be_nil + rescue StandardError + # Expected error — test passes + end + end + end + + describe 'SearchOffsetAndNextError' do + it 'verifies error when using offset with next' do + expect do + search_messages( + filter_conditions: { 'members' => { '$in' => [@user1] } }, + query: 'test', + offset: 1, + next: SecureRandom.hex(5) + ) + end.to raise_error(StandardError) + end + end + + describe 'ChannelRoleInMember' do + it 'creates channel with roles, sends messages, verifies member.channel_role in response' do + role_user_ids, _resp = create_test_users(2) + member_user_id = role_user_ids[0] + mod_user_id = role_user_ids[1] + + channel_id = "test-ch-#{SecureRandom.hex(6)}" + @client.make_request( + :post, + "/api/v2/chat/channels/messaging/#{channel_id}/query", + body: { + data: { + created_by_id: member_user_id, + members: [ + { user_id: member_user_id, channel_role: 'channel_member' }, + { user_id: mod_user_id, channel_role: 'channel_moderator' } + ] + } + } + ) + @created_channel_cids << "messaging:#{channel_id}" + + # Send message from channel_member + resp_member = send_msg('messaging', channel_id, + message: { text: 'message from channel_member', user_id: member_user_id }) + expect(resp_member.message).not_to be_nil + member_data = resp_member.message.to_h['member'] || {} + expect(member_data['channel_role']).to eq('channel_member') + + # Send message from channel_moderator + resp_mod = send_msg('messaging', channel_id, + message: { text: 'message from channel_moderator', user_id: mod_user_id }) + expect(resp_mod.message).not_to be_nil + mod_data = resp_mod.message.to_h['member'] || {} + expect(mod_data['channel_role']).to eq('channel_moderator') + end + end +end From 604b97478962135acd52784ceea9abc00213207d Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 12:04:30 +0100 Subject: [PATCH 03/29] feat: add 2 polls integration tests for chat test parity Co-Authored-By: Claude Opus 4.6 --- .../chat_polls_integration_spec.rb | 169 ++++++++++++++++++ .../chat_reaction_integration_spec.rb | 115 ++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 spec/integration/chat_polls_integration_spec.rb create mode 100644 spec/integration/chat_reaction_integration_spec.rb diff --git a/spec/integration/chat_polls_integration_spec.rb b/spec/integration/chat_polls_integration_spec.rb new file mode 100644 index 0000000..6916f48 --- /dev/null +++ b/spec/integration/chat_polls_integration_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Polls Integration', type: :integration do + include ChatTestHelpers + + before(:all) do + init_chat_client + @shared_user_ids, _resp = create_test_users(2) + @user1 = @shared_user_ids[0] + @user2 = @shared_user_ids[1] + @created_poll_ids = [] + end + + after(:all) do + # Delete polls before channels/users (polls reference users) + @created_poll_ids&.each do |poll_id| + @client.common.delete_poll(poll_id, @user1) + rescue StandardError => e + puts "Warning: Failed to delete poll #{poll_id}: #{e.message}" + end + + cleanup_chat_resources + end + + # --------------------------------------------------------------------------- + # Poll API wrappers + # --------------------------------------------------------------------------- + + def create_poll(name, user_id, options: [], enforce_unique_vote: nil, description: nil) + poll_options = options.map do |text| + GetStream::Generated::Models::PollOptionInput.new(text: text) + end + + req = GetStream::Generated::Models::CreatePollRequest.new( + name: name, + user_id: user_id, + options: poll_options, + enforce_unique_vote: enforce_unique_vote, + description: description + ) + + resp = @client.common.create_poll(req) + poll_id = resp.poll.id + @created_poll_ids << poll_id + resp + end + + def get_poll(poll_id) + @client.common.get_poll(poll_id) + end + + def query_polls(filter, user_id) + req = GetStream::Generated::Models::QueryPollsRequest.new(filter: filter) + @client.common.query_polls(req, user_id) + end + + def delete_poll(poll_id, user_id) + @client.common.delete_poll(poll_id, user_id) + end + + def cast_poll_vote(message_id, poll_id, user_id, option_id) + body = { + user_id: user_id, + vote: { option_id: option_id } + } + @client.make_request( + :post, + "/api/v2/chat/messages/#{message_id}/polls/#{poll_id}/vote", + body: body + ) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'CreateAndQueryPoll' do + it 'creates a poll with options, gets it, and queries it' do + poll_name = "Favorite color? #{SecureRandom.hex(4)}" + + # Create poll with options + create_resp = create_poll( + poll_name, + @user1, + options: %w[Red Blue Green], + enforce_unique_vote: true, + description: 'Pick your favorite color' + ) + expect(create_resp.poll).not_to be_nil + poll_id = create_resp.poll.id + expect(poll_id).not_to be_nil + expect(create_resp.poll.name).to eq(poll_name) + expect(create_resp.poll.enforce_unique_vote).to eq(true) + + poll_h = create_resp.poll.to_h + expect(poll_h['options'].length).to eq(3) + + # Get poll by ID + get_resp = get_poll(poll_id) + expect(get_resp.poll).not_to be_nil + expect(get_resp.poll.id).to eq(poll_id) + expect(get_resp.poll.name).to eq(poll_name) + + # Query polls with filter + query_resp = query_polls({ 'id' => poll_id }, @user1) + expect(query_resp.polls).not_to be_nil + expect(query_resp.polls.length).to be >= 1 + + found = query_resp.polls.any? do |p| + h = p.is_a?(Hash) ? p : p.to_h + h['id'] == poll_id + end + expect(found).to be true + rescue StandardError => e + skip('Polls not enabled for this app') if e.message.include?('Polls') || e.message.include?('polls') + raise + end + end + + describe 'CastPollVote' do + it 'creates a poll, attaches to message, casts vote, and verifies' do + # Create poll + poll_name = "Vote test #{SecureRandom.hex(4)}" + create_resp = create_poll( + poll_name, + @user1, + options: %w[Yes No], + enforce_unique_vote: true + ) + poll_id = create_resp.poll.id + poll_h = create_resp.poll.to_h + option_id = poll_h['options'][0]['id'] + expect(option_id).not_to be_nil + + # Create channel with both users as members + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + # Send message with poll attached + body = { + message: { + text: 'Please vote!', + user_id: @user1, + poll_id: poll_id + } + } + msg_resp = send_message('messaging', channel_id, body) + msg_id = msg_resp.message.id + expect(msg_id).not_to be_nil + + # Cast a vote as user2 + vote_resp = cast_poll_vote(msg_id, poll_id, @user2, option_id) + expect(vote_resp.vote).not_to be_nil + vote_h = vote_resp.vote.to_h + expect(vote_h['option_id']).to eq(option_id) + + # Verify poll has votes + get_resp = get_poll(poll_id) + expect(get_resp.poll.vote_count).to eq(1) + rescue StandardError => e + skip('Polls not enabled for this app') if e.message.include?('Polls') || e.message.include?('polls') + raise + end + end +end diff --git a/spec/integration/chat_reaction_integration_spec.rb b/spec/integration/chat_reaction_integration_spec.rb new file mode 100644 index 0000000..7470d0b --- /dev/null +++ b/spec/integration/chat_reaction_integration_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Reaction Integration', type: :integration do + include ChatTestHelpers + + before(:all) do + init_chat_client + @shared_user_ids, _resp = create_test_users(2) + @user1 = @shared_user_ids[0] + @user2 = @shared_user_ids[1] + end + + after(:all) do + cleanup_chat_resources + end + + # --------------------------------------------------------------------------- + # Reaction API wrappers + # --------------------------------------------------------------------------- + + def send_reaction(message_id, reaction_type, user_id, enforce_unique: false) + body = { + reaction: { type: reaction_type, user_id: user_id } + } + body[:enforce_unique] = true if enforce_unique + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/reaction", body: body) + end + + def get_reactions(message_id) + @client.make_request(:get, "/api/v2/chat/messages/#{message_id}/reactions") + end + + def delete_reaction(message_id, reaction_type, user_id) + @client.make_request( + :delete, + "/api/v2/chat/messages/#{message_id}/reaction/#{reaction_type}", + query_params: { 'user_id' => user_id } + ) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'SendAndGetReactions' do + it 'sends reactions and gets them back' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + msg_id = send_test_message('messaging', channel_id, @user1, "React to this #{SecureRandom.hex(8)}") + + # Send two reactions from different users + resp1 = send_reaction(msg_id, 'like', @user1) + expect(resp1.reaction).not_to be_nil + expect(resp1.reaction.to_h['type']).to eq('like') + expect(resp1.reaction.to_h['user_id']).to eq(@user1) + + resp2 = send_reaction(msg_id, 'love', @user2) + expect(resp2.reaction).not_to be_nil + expect(resp2.reaction.to_h['type']).to eq('love') + expect(resp2.reaction.to_h['user_id']).to eq(@user2) + + # Get reactions + get_resp = get_reactions(msg_id) + expect(get_resp.reactions).not_to be_nil + expect(get_resp.reactions.length).to be >= 2 + end + end + + describe 'DeleteReaction' do + it 'sends a reaction, deletes it, and verifies removal' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, "Delete reaction test #{SecureRandom.hex(8)}") + + # Send reaction + send_reaction(msg_id, 'like', @user1) + + # Delete reaction + del_resp = delete_reaction(msg_id, 'like', @user1) + expect(del_resp).not_to be_nil + + # Verify reaction is gone + get_resp = get_reactions(msg_id) + user_likes = (get_resp.reactions || []).select do |r| + h = r.is_a?(Hash) ? r : r.to_h + h['user_id'] == @user1 && h['type'] == 'like' + end + expect(user_likes.length).to eq(0) + end + end + + describe 'EnforceUniqueReaction' do + it 'enforces only one reaction per user when enforce_unique is set' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, "Unique reaction test #{SecureRandom.hex(8)}") + + # Send first reaction with enforce_unique + send_reaction(msg_id, 'like', @user1, enforce_unique: true) + + # Send second reaction with enforce_unique — should replace, not duplicate + send_reaction(msg_id, 'love', @user1, enforce_unique: true) + + # Verify user has only one reaction + get_resp = get_reactions(msg_id) + user_reactions = (get_resp.reactions || []).select do |r| + h = r.is_a?(Hash) ? r : r.to_h + h['user_id'] == @user1 + end + expect(user_reactions.length).to eq(1) + end + end +end From 9f72360ee24e4a8807d5583c7435ac20ab0e5131 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 12:14:43 +0100 Subject: [PATCH 04/29] feat: add 19 misc chat integration tests for chat test parity Co-Authored-By: Claude Opus 4.6 --- .../integration/chat_misc_integration_spec.rb | 641 ++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 spec/integration/chat_misc_integration_spec.rb diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb new file mode 100644 index 0000000..825d1c8 --- /dev/null +++ b/spec/integration/chat_misc_integration_spec.rb @@ -0,0 +1,641 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Misc Integration', type: :integration do + include ChatTestHelpers + + before(:all) do + init_chat_client + @shared_user_ids, _resp = create_test_users(4) + @user1 = @shared_user_ids[0] + @user2 = @shared_user_ids[1] + @user3 = @shared_user_ids[2] + @user4 = @shared_user_ids[3] + @created_blocklist_names = [] + @created_command_names = [] + @created_channel_type_names = [] + @created_role_names = [] + end + + after(:all) do + # Clean up blocklists + @created_blocklist_names&.each do |name| + @client.common.delete_block_list(name) + rescue StandardError => e + puts "Warning: Failed to delete blocklist #{name}: #{e.message}" + end + + # Clean up commands + @created_command_names&.each do |name| + @client.make_request(:delete, "/api/v2/chat/commands/#{name}") + rescue StandardError => e + puts "Warning: Failed to delete command #{name}: #{e.message}" + end + + # Clean up channel types (with retry due to eventual consistency) + @created_channel_type_names&.each do |name| + 5.times do |i| + @client.make_request(:delete, "/api/v2/chat/channeltypes/#{name}") + break + rescue StandardError => e + puts "Warning: Failed to delete channel type #{name} (attempt #{i + 1}): #{e.message}" + sleep(2) + end + end + + # Clean up roles + @created_role_names&.each do |name| + sleep(2) + 5.times do |i| + @client.common.delete_role(name) + break + rescue StandardError => e + puts "Warning: Failed to delete role #{name} (attempt #{i + 1}): #{e.message}" + sleep(1) + end + end + + cleanup_chat_resources + end + + # --------------------------------------------------------------------------- + # Devices + # --------------------------------------------------------------------------- + + describe 'CreateListDeleteDevice' do + it 'creates a firebase device, lists it, deletes it, and verifies gone' do + device_id = "integration-test-device-#{random_string(12)}" + + # Create device + @client.common.create_device( + GetStream::Generated::Models::CreateDeviceRequest.new( + id: device_id, + push_provider: 'firebase', + user_id: @user1 + ) + ) + + # List devices + list_resp = @client.common.list_devices(@user1) + devices = list_resp.devices || [] + found = devices.any? { |d| h = d.is_a?(Hash) ? d : d.to_h; h['id'] == device_id } + expect(found).to be(true), "Created device should appear in list" + + # Delete device + @client.common.delete_device(device_id, @user1) + + # Verify deleted + list_resp2 = @client.common.list_devices(@user1) + devices2 = list_resp2.devices || [] + still_found = devices2.any? { |d| h = d.is_a?(Hash) ? d : d.to_h; h['id'] == device_id } + expect(still_found).to be(false), "Device should be deleted" + rescue GetStreamRuby::APIError => e + skip('Push providers not configured for this app') if e.message.include?('push provider') || e.message.include?('no push') + raise + end + end + + # --------------------------------------------------------------------------- + # Blocklists + # --------------------------------------------------------------------------- + + describe 'CreateListDeleteBlocklist' do + it 'creates a custom blocklist, lists it, verifies found, and deletes it' do + blocklist_name = "test-blocklist-#{random_string(8)}" + + # Create blocklist + @client.common.create_block_list( + GetStream::Generated::Models::CreateBlockListRequest.new( + name: blocklist_name, + words: %w[badword1 badword2 badword3] + ) + ) + @created_blocklist_names << blocklist_name + + # Get blocklist and verify + get_resp = @client.common.get_block_list(blocklist_name) + expect(get_resp.blocklist).not_to be_nil + bl_h = get_resp.blocklist.to_h + expect(bl_h['name']).to eq(blocklist_name) + expect(bl_h['words'].length).to eq(3) + + # Update blocklist + @client.common.update_block_list( + blocklist_name, + GetStream::Generated::Models::UpdateBlockListRequest.new( + words: %w[badword1 badword2 badword3 badword4] + ) + ) + + # Verify update + get_resp2 = @client.common.get_block_list(blocklist_name) + bl_h2 = get_resp2.blocklist.to_h + expect(bl_h2['words'].length).to eq(4) + + # List blocklists and verify found + list_resp = @client.common.list_block_lists + blocklists = list_resp.blocklists || [] + found = blocklists.any? do |bl| + h = bl.is_a?(Hash) ? bl : bl.to_h + h['name'] == blocklist_name + end + expect(found).to be(true), "Created blocklist should appear in list" + + # Delete a separate blocklist to test deletion + del_name = "test-del-bl-#{random_string(8)}" + @client.common.create_block_list( + GetStream::Generated::Models::CreateBlockListRequest.new( + name: del_name, + words: %w[word1] + ) + ) + @client.common.delete_block_list(del_name) + end + end + + # --------------------------------------------------------------------------- + # Commands + # --------------------------------------------------------------------------- + + describe 'CreateListDeleteCommand' do + it 'creates a custom command, lists it, verifies found, and deletes it' do + cmd_name = "testcmd#{random_string(6)}" + + # Create command + resp = @client.make_request(:post, '/api/v2/chat/commands', body: { + name: cmd_name, + description: 'A test command' + }) + expect(resp).not_to be_nil + @created_command_names << cmd_name + + # Get command + get_resp = @client.make_request(:get, "/api/v2/chat/commands/#{cmd_name}") + expect(get_resp.name).to eq(cmd_name) + expect(get_resp.description).to eq('A test command') + + # Update command + @client.make_request(:put, "/api/v2/chat/commands/#{cmd_name}", body: { + description: 'Updated test command' + }) + + # Verify update + get_resp2 = @client.make_request(:get, "/api/v2/chat/commands/#{cmd_name}") + expect(get_resp2.description).to eq('Updated test command') + + # List commands + list_resp = @client.make_request(:get, '/api/v2/chat/commands') + commands = list_resp.commands || [] + found = commands.any? do |c| + h = c.is_a?(Hash) ? c : c.to_h + h['name'] == cmd_name + end + expect(found).to be(true), "Created command should appear in list" + + # Delete a separate command + del_name = "testdelcmd#{random_string(6)}" + @client.make_request(:post, '/api/v2/chat/commands', body: { + name: del_name, + description: 'Command to delete' + }) + del_resp = @client.make_request(:delete, "/api/v2/chat/commands/#{del_name}") + expect(del_resp).not_to be_nil + end + end + + # --------------------------------------------------------------------------- + # Channel Types + # --------------------------------------------------------------------------- + + describe 'CreateUpdateDeleteChannelType' do + it 'creates a channel type, updates settings, verifies, and deletes' do + type_name = "testtype#{random_string(6)}" + + # Create channel type + create_resp = @client.make_request(:post, '/api/v2/chat/channeltypes', body: { + name: type_name, + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 5000 + }) + expect(create_resp.name).to eq(type_name) + @created_channel_type_names << type_name + + # Wait for eventual consistency + sleep(6) + + # Get channel type + get_resp = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") + expect(get_resp.name).to eq(type_name) + + # Update channel type + update_resp = @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 10_000, + typing_events: false + }) + expect(update_resp.max_message_length).to eq(10_000) + + # Delete a separate channel type + del_name = "testdeltype#{random_string(6)}" + @client.make_request(:post, '/api/v2/chat/channeltypes', body: { + name: del_name, + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 5000 + }) + @created_channel_type_names << del_name + + sleep(6) + + delete_err = nil + 5.times do |i| + begin + @client.make_request(:delete, "/api/v2/chat/channeltypes/#{del_name}") + @created_channel_type_names.delete(del_name) + delete_err = nil + break + rescue StandardError => e + delete_err = e + sleep(1) + end + end + expect(delete_err).to be_nil, "Channel type deletion should succeed: #{delete_err&.message}" + end + end + + describe 'ListChannelTypes' do + it 'lists all channel types and verifies default types present' do + resp = @client.make_request(:get, '/api/v2/chat/channeltypes') + expect(resp.channel_types).not_to be_nil + + types_h = resp.channel_types.to_h + expect(types_h.key?('messaging')).to be(true), "Default 'messaging' type should be present" + end + end + + # --------------------------------------------------------------------------- + # Permissions & Roles + # --------------------------------------------------------------------------- + + describe 'ListPermissions' do + it 'lists all permissions and verifies non-empty' do + resp = @client.common.list_permissions + expect(resp.permissions).not_to be_nil + expect(resp.permissions.length).to be > 0 + end + end + + describe 'CreatePermission' do + it 'creates a custom role, lists it, and verifies custom flag' do + role_name = "testrole#{random_string(6)}" + + # Create role + @client.common.create_role( + GetStream::Generated::Models::CreateRoleRequest.new(name: role_name) + ) + @created_role_names << role_name + + # List roles and verify + list_resp = @client.common.list_roles + roles = list_resp.roles || [] + found = roles.any? do |r| + h = r.is_a?(Hash) ? r : r.to_h + h['name'] == role_name && h['custom'] == true + end + expect(found).to be(true), "Created role should appear in list as custom" + end + end + + describe 'GetPermission' do + it 'gets a specific permission by ID' do + resp = @client.common.get_permission('create-channel') + expect(resp.permission).not_to be_nil + perm_h = resp.permission.to_h + expect(perm_h['id']).to eq('create-channel') + expect(perm_h['action']).not_to be_nil + expect(perm_h['action']).not_to be_empty + end + end + + # --------------------------------------------------------------------------- + # Banned Users + # --------------------------------------------------------------------------- + + describe 'QueryBannedUsers' do + it 'bans a user in channel, queries banned users, and verifies' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + cid = "messaging:#{channel_id}" + + # Ban user in channel + @client.moderation.ban( + GetStream::Generated::Models::BanRequest.new( + target_user_id: @user2, + banned_by_id: @user1, + channel_cid: cid, + reason: 'test ban reason', + timeout: 60 + ) + ) + + # Query banned users + resp = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { + 'payload' => JSON.generate({ + filter_conditions: { 'channel_cid' => { '$eq' => cid } } + }) + }) + bans = resp.bans || [] + expect(bans.length).to be >= 1 + + ban_h = bans[0].is_a?(Hash) ? bans[0] : bans[0].to_h + expect(ban_h['reason']).to eq('test ban reason') + + # Unban + @client.moderation.unban( + GetStream::Generated::Models::UnbanRequest.new, + @user2, + cid + ) + + # Verify ban is gone + resp2 = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { + 'payload' => JSON.generate({ + filter_conditions: { 'channel_cid' => { '$eq' => cid } } + }) + }) + bans2 = resp2.bans || [] + expect(bans2.length).to eq(0), "Bans should be empty after unban" + end + end + + # --------------------------------------------------------------------------- + # Mute/Unmute User + # --------------------------------------------------------------------------- + + describe 'MuteUnmuteUser' do + it 'mutes user, verifies via query, and unmutes' do + # Mute user + mute_resp = @client.moderation.mute( + GetStream::Generated::Models::MuteRequest.new( + target_ids: [@user3], + user_id: @user1 + ) + ) + expect(mute_resp.mutes).not_to be_nil + expect(mute_resp.mutes.length).to be >= 1 + + mute_h = mute_resp.mutes[0].is_a?(Hash) ? mute_resp.mutes[0] : mute_resp.mutes[0].to_h + expect(mute_h['target']).not_to be_nil + + # Verify via QueryUsers that user has mutes + q_resp = @client.common.query_users(JSON.generate({ + filter_conditions: { 'id' => { '$eq' => @user1 } } + })) + expect(q_resp.users).not_to be_nil + expect(q_resp.users.length).to be >= 1 + user_h = q_resp.users[0].is_a?(Hash) ? q_resp.users[0] : q_resp.users[0].to_h + expect(user_h['mutes']).not_to be_nil + expect(user_h['mutes'].length).to be >= 1 + + # Unmute + @client.moderation.unmute( + GetStream::Generated::Models::UnmuteRequest.new( + target_ids: [@user3], + user_id: @user1 + ) + ) + end + end + + # --------------------------------------------------------------------------- + # App Settings + # --------------------------------------------------------------------------- + + describe 'GetAppSettings' do + it 'gets app settings and verifies response' do + resp = @client.common.get_app + expect(resp).not_to be_nil + expect(resp.app).not_to be_nil + app_h = resp.app.to_h + expect(app_h['name']).not_to be_nil + expect(app_h['name']).not_to be_empty + end + end + + # --------------------------------------------------------------------------- + # Export Channels + # --------------------------------------------------------------------------- + + describe 'ExportChannels' do + it 'exports channel messages and polls task until completed' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + send_test_message('messaging', channel_id, @user1, "Message for export test #{SecureRandom.hex(4)}") + + cid = "messaging:#{channel_id}" + + # Export channels + export_resp = @client.make_request(:post, '/api/v2/chat/export_channels', body: { + channels: [{ cid: cid }] + }) + expect(export_resp.task_id).not_to be_nil + expect(export_resp.task_id).not_to be_empty + + # Wait for task + task_result = wait_for_task(export_resp.task_id) + expect(task_result.status).to eq('completed') + end + end + + # --------------------------------------------------------------------------- + # Threads + # --------------------------------------------------------------------------- + + describe 'Threads' do + it 'creates parent + replies, queries threads, and verifies' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + channel_cid = "messaging:#{channel_id}" + + # Create thread: parent message + replies + parent_id = send_test_message('messaging', channel_id, @user1, 'Thread parent message') + + send_message('messaging', channel_id, { + message: { + text: 'First reply in thread', + user_id: @user2, + parent_id: parent_id + } + }) + + send_message('messaging', channel_id, { + message: { + text: 'Second reply in thread', + user_id: @user1, + parent_id: parent_id + } + }) + + # Query threads + resp = @client.make_request(:post, '/api/v2/chat/threads', body: { + user_id: @user1, + filter: { + 'channel_cid' => { '$eq' => channel_cid } + } + }) + expect(resp.threads).not_to be_nil + expect(resp.threads.length).to be >= 1 + + found = resp.threads.any? do |t| + h = t.is_a?(Hash) ? t : t.to_h + h['parent_message_id'] == parent_id + end + expect(found).to be(true), "Thread should appear in query results" + + # Get thread + get_resp = @client.make_request(:get, "/api/v2/chat/threads/#{parent_id}", query_params: { + 'reply_limit' => '10' + }) + thread_h = get_resp.thread.is_a?(Hash) ? get_resp.thread : get_resp.thread.to_h + expect(thread_h['parent_message_id']).to eq(parent_id) + latest_replies = thread_h['latest_replies'] || [] + expect(latest_replies.length).to be >= 2 + end + end + + # --------------------------------------------------------------------------- + # Unread Counts + # --------------------------------------------------------------------------- + + describe 'GetUnreadCounts' do + it 'sends message and gets unread counts for user' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + send_test_message('messaging', channel_id, @user1, "Unread test #{SecureRandom.hex(4)}") + + resp = @client.make_request(:get, '/api/v2/chat/unread', query_params: { + 'user_id' => @user2 + }) + expect(resp).not_to be_nil + expect(resp.total_unread_count).to be >= 0 + end + end + + describe 'GetUnreadCountsBatch' do + it 'gets unread counts for multiple users' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + send_test_message('messaging', channel_id, @user1, "Batch unread test #{SecureRandom.hex(4)}") + + resp = @client.make_request(:post, '/api/v2/chat/unread_batch', body: { + user_ids: [@user1, @user2] + }) + expect(resp).not_to be_nil + expect(resp.counts_by_user).not_to be_nil + counts_h = resp.counts_by_user.to_h + expect(counts_h.key?(@user1)).to be(true) + expect(counts_h.key?(@user2)).to be(true) + end + end + + # --------------------------------------------------------------------------- + # Reminders + # --------------------------------------------------------------------------- + + describe 'Reminders' do + it 'creates a reminder, lists it, updates it, and deletes it' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + msg_id = send_test_message('messaging', channel_id, @user1, "Reminder test #{SecureRandom.hex(4)}") + + remind_at = (Time.now + 24 * 3600).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') + + # Create reminder + create_resp = @client.make_request(:post, "/api/v2/chat/messages/#{msg_id}/reminders", body: { + user_id: @user1, + remind_at: remind_at + }) + expect(create_resp).not_to be_nil + + # Query reminders + query_resp = @client.make_request(:post, '/api/v2/chat/reminders/query', body: { + user_id: @user1, + filter: { 'message_id' => msg_id }, + sort: [] + }) + reminders = query_resp.reminders || [] + expect(reminders.length).to be >= 1 + + # Update reminder + new_remind_at = (Time.now + 48 * 3600).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') + update_resp = @client.make_request(:patch, "/api/v2/chat/messages/#{msg_id}/reminders", body: { + user_id: @user1, + remind_at: new_remind_at + }) + expect(update_resp).not_to be_nil + + # Delete reminder + @client.make_request(:delete, "/api/v2/chat/messages/#{msg_id}/reminders", query_params: { + 'user_id' => @user1 + }) + rescue GetStreamRuby::APIError => e + skip('Reminders not enabled for this app') if e.message.include?('not enabled') || e.message.include?('reminder') + raise + end + end + + # --------------------------------------------------------------------------- + # Send User Custom Event + # --------------------------------------------------------------------------- + + describe 'SendUserCustomEvent' do + it 'sends a custom event to a user' do + resp = @client.make_request(:post, "/api/v2/chat/users/#{@user1}/event", body: { + event: { + type: 'friendship_request', + message: "Let's be friends!" + } + }) + expect(resp).not_to be_nil + end + end + + # --------------------------------------------------------------------------- + # Query Team Usage Stats + # --------------------------------------------------------------------------- + + describe 'QueryTeamUsageStats' do + it 'queries team usage stats' do + resp = @client.make_request(:post, '/api/v2/chat/stats/team_usage', body: {}) + expect(resp).not_to be_nil + rescue GetStreamRuby::APIError => e + skip('QueryTeamUsageStats not available on this app') if e.message.include?('Token signature') || e.message.include?('not available') || e.message.include?('not found') || e.message.include?('Not Found') + raise + end + end + + # --------------------------------------------------------------------------- + # Channel Batch Update + # --------------------------------------------------------------------------- + + describe 'ChannelBatchUpdate' do + it 'batch updates multiple channels at once' do + _type1, ch_id1, _resp1 = create_test_channel(@user1) + _type2, ch_id2, _resp2 = create_test_channel(@user1) + + # Batch update: set a custom field on both channels + cids = ["messaging:#{ch_id1}", "messaging:#{ch_id2}"] + + resp = @client.make_request(:post, '/api/v2/chat/channels/batch_update', body: { + set: { 'color' => 'blue' }, + filter: { + 'cid' => { '$in' => cids } + } + }) + expect(resp).not_to be_nil + rescue GetStreamRuby::APIError => e + skip('Channel batch update not available') if e.message.include?('not available') || e.message.include?('Not Found') || e.message.include?('unknown') || e.message.include?('not found') + raise + end + end +end From 14f5b98e14a3a3c6350f5a5a1d3e232efb0f1ef3 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 12:19:07 +0100 Subject: [PATCH 05/29] feat: add 3 moderation integration tests for chat test parity Co-Authored-By: Claude Opus 4.6 --- .../chat_moderation_integration_spec.rb | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 spec/integration/chat_moderation_integration_spec.rb diff --git a/spec/integration/chat_moderation_integration_spec.rb b/spec/integration/chat_moderation_integration_spec.rb new file mode 100644 index 0000000..e83c9c5 --- /dev/null +++ b/spec/integration/chat_moderation_integration_spec.rb @@ -0,0 +1,220 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Moderation Integration', type: :integration do + include ChatTestHelpers + + before(:all) do + init_chat_client + @shared_user_ids, _resp = create_test_users(4) + @user1 = @shared_user_ids[0] + @user2 = @shared_user_ids[1] + @user3 = @shared_user_ids[2] + @user4 = @shared_user_ids[3] + end + + after(:all) do + cleanup_chat_resources + end + + # --------------------------------------------------------------------------- + # Ban / Unban User + # --------------------------------------------------------------------------- + + describe 'BanUnbanUser' do + it 'bans a user from a channel, verifies, and unbans' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + cid = "messaging:#{channel_id}" + + # Ban user in channel + @client.moderation.ban( + GetStream::Generated::Models::BanRequest.new( + target_user_id: @user2, + banned_by_id: @user1, + channel_cid: cid, + reason: 'moderation test ban', + timeout: 60 + ) + ) + + # Verify via query banned users + resp = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { + 'payload' => JSON.generate({ + filter_conditions: { 'channel_cid' => { '$eq' => cid } } + }) + }) + bans = resp.bans || [] + expect(bans.length).to be >= 1 + + banned_user_ids = bans.map do |b| + h = b.is_a?(Hash) ? b : b.to_h + target = h['user'] || {} + target = target.is_a?(Hash) ? target : target.to_h + target['id'] + end + expect(banned_user_ids).to include(@user2) + + # Unban user + @client.moderation.unban( + GetStream::Generated::Models::UnbanRequest.new, + @user2, + cid + ) + + # Verify ban is removed + resp2 = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { + 'payload' => JSON.generate({ + filter_conditions: { 'channel_cid' => { '$eq' => cid } } + }) + }) + bans2 = resp2.bans || [] + banned_ids_after = bans2.map do |b| + h = b.is_a?(Hash) ? b : b.to_h + target = h['user'] || {} + target = target.is_a?(Hash) ? target : target.to_h + target['id'] + end + expect(banned_ids_after).not_to include(@user2) + end + + it 'bans a user app-wide, verifies, and unbans' do + # Ban user app-wide (no channel_cid) + @client.moderation.ban( + GetStream::Generated::Models::BanRequest.new( + target_user_id: @user3, + banned_by_id: @user1, + reason: 'app-wide moderation test ban', + timeout: 60 + ) + ) + + # Verify via query banned users (app-level) + resp = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { + 'payload' => JSON.generate({ + filter_conditions: { 'user_id' => { '$eq' => @user3 } } + }) + }) + bans = resp.bans || [] + expect(bans.length).to be >= 1 + + # Unban user app-wide + @client.moderation.unban( + GetStream::Generated::Models::UnbanRequest.new, + @user3 + ) + + # Verify ban is removed + resp2 = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { + 'payload' => JSON.generate({ + filter_conditions: { 'user_id' => { '$eq' => @user3 } } + }) + }) + bans2 = resp2.bans || [] + expect(bans2.length).to eq(0), "App-wide ban should be removed after unban" + end + end + + # --------------------------------------------------------------------------- + # Mute / Unmute User + # --------------------------------------------------------------------------- + + describe 'MuteUnmuteUser' do + it 'mutes a user, verifies via query, and unmutes' do + # Mute user + mute_resp = @client.moderation.mute( + GetStream::Generated::Models::MuteRequest.new( + target_ids: [@user4], + user_id: @user1 + ) + ) + expect(mute_resp.mutes).not_to be_nil + expect(mute_resp.mutes.length).to be >= 1 + + mute_h = mute_resp.mutes[0].is_a?(Hash) ? mute_resp.mutes[0] : mute_resp.mutes[0].to_h + target = mute_h['target'] || {} + target = target.is_a?(Hash) ? target : target.to_h + expect(target['id']).to eq(@user4) + + # Verify via QueryUsers that muter has mutes + q_resp = @client.common.query_users(JSON.generate({ + filter_conditions: { 'id' => { '$eq' => @user1 } } + })) + expect(q_resp.users).not_to be_nil + expect(q_resp.users.length).to be >= 1 + user_h = q_resp.users[0].is_a?(Hash) ? q_resp.users[0] : q_resp.users[0].to_h + expect(user_h['mutes']).not_to be_nil + expect(user_h['mutes'].length).to be >= 1 + + muted_ids = user_h['mutes'].map do |m| + t = m.is_a?(Hash) ? m : m.to_h + tgt = t['target'] || {} + tgt = tgt.is_a?(Hash) ? tgt : tgt.to_h + tgt['id'] + end + expect(muted_ids).to include(@user4) + + # Unmute user + @client.moderation.unmute( + GetStream::Generated::Models::UnmuteRequest.new( + target_ids: [@user4], + user_id: @user1 + ) + ) + + # Verify mute is removed + q_resp2 = @client.common.query_users(JSON.generate({ + filter_conditions: { 'id' => { '$eq' => @user1 } } + })) + user_h2 = q_resp2.users[0].is_a?(Hash) ? q_resp2.users[0] : q_resp2.users[0].to_h + mutes_after = user_h2['mutes'] || [] + muted_ids_after = mutes_after.map do |m| + t = m.is_a?(Hash) ? m : m.to_h + tgt = t['target'] || {} + tgt = tgt.is_a?(Hash) ? tgt : tgt.to_h + tgt['id'] + end + expect(muted_ids_after).not_to include(@user4) + end + end + + # --------------------------------------------------------------------------- + # Flag Message and User + # --------------------------------------------------------------------------- + + describe 'FlagMessageAndUser' do + it 'flags a message and verifies response' do + _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + msg_id = send_test_message('messaging', channel_id, @user1, "Flaggable message #{SecureRandom.hex(4)}") + + # Flag message + flag_resp = @client.moderation.flag( + GetStream::Generated::Models::FlagRequest.new( + entity_type: 'stream:chat:v1:message', + entity_id: msg_id, + entity_creator_id: @user1, + reason: 'inappropriate content', + user_id: @user2 + ) + ) + expect(flag_resp).not_to be_nil + end + + it 'flags a user and verifies response' do + # Flag user + flag_resp = @client.moderation.flag( + GetStream::Generated::Models::FlagRequest.new( + entity_type: 'stream:user', + entity_id: @user3, + entity_creator_id: @user3, + reason: 'spam behavior', + user_id: @user1 + ) + ) + expect(flag_resp).not_to be_nil + end + end +end From b2eed217161a58aa756d4fd061efbb29042dbc3c Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 13:39:53 +0100 Subject: [PATCH 06/29] feat: add 18 video integration tests for SDK test parity Co-Authored-By: Claude Opus 4.6 --- spec/integration/video_integration_spec.rb | 744 +++++++++++++++++++++ 1 file changed, 744 insertions(+) create mode 100644 spec/integration/video_integration_spec.rb diff --git a/spec/integration/video_integration_spec.rb b/spec/integration/video_integration_spec.rb new file mode 100644 index 0000000..7b74d72 --- /dev/null +++ b/spec/integration/video_integration_spec.rb @@ -0,0 +1,744 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Video Integration', type: :integration do + include ChatTestHelpers + + before(:all) do + init_chat_client + @created_call_type_names = [] + @created_call_ids = [] # [call_type, call_id] pairs + @shared_user_ids, _resp = create_test_users(4) + @user1 = @shared_user_ids[0] + @user2 = @shared_user_ids[1] + @user3 = @shared_user_ids[2] + @user4 = @shared_user_ids[3] + end + + after(:all) do + # Clean up calls (soft delete) + @created_call_ids&.each do |call_type, call_id| + @client.make_request( + :post, + "/api/v2/video/call/#{call_type}/#{call_id}/delete", + body: {} + ) + rescue StandardError => e + puts "Warning: Failed to delete call #{call_type}:#{call_id}: #{e.message}" + end + + # Clean up call types + @created_call_type_names&.each do |name| + sleep(1) # small delay to avoid rate limits + @client.make_request(:delete, "/api/v2/video/calltypes/#{name}") + rescue StandardError => e + puts "Warning: Failed to delete call type #{name}: #{e.message}" + end + + cleanup_chat_resources + end + + # --------------------------------------------------------------------------- + # Helpers + # --------------------------------------------------------------------------- + + def create_call(call_type, call_id, body = {}) + @client.make_request(:post, "/api/v2/video/call/#{call_type}/#{call_id}", body: body) + end + + def get_call(call_type, call_id) + @client.make_request(:get, "/api/v2/video/call/#{call_type}/#{call_id}") + end + + def update_call(call_type, call_id, body) + @client.make_request(:patch, "/api/v2/video/call/#{call_type}/#{call_id}", body: body) + end + + def delete_call_req(call_type, call_id, body = {}) + @client.make_request(:post, "/api/v2/video/call/#{call_type}/#{call_id}/delete", body: body) + end + + def new_call_id + "test-call-#{random_string(10)}" + end + + def new_call_type_name + "testct#{random_string(8)}" + end + + # --------------------------------------------------------------------------- + # CRUDCallTypeOperations + # --------------------------------------------------------------------------- + + describe 'CRUDCallTypeOperations' do + it 'creates a call type with settings, updates, reads, and deletes' do + ct_name = new_call_type_name + @created_call_type_names << ct_name + + # Create call type + resp = @client.make_request(:post, '/api/v2/video/calltypes', body: { + name: ct_name, + grants: { + 'admin' => %w[send-audio send-video mute-users], + 'user' => %w[send-audio send-video] + }, + settings: { + audio: { default_device: 'speaker', mic_default_on: true }, + screensharing: { access_request_enabled: false, enabled: true } + }, + notification_settings: { + enabled: true, + call_notification: { + enabled: true, + apns: { title: '{{ user.display_name }} invites you to a call', body: '' } + }, + session_started: { enabled: false }, + call_live_started: { enabled: false }, + call_ring: { enabled: false } + } + }) + expect(resp.name).to eq(ct_name) + + # Wait for eventual consistency (video call types need longer propagation than chat channel types) + sleep(20) + + # Update call type settings (with retry for eventual consistency) + resp2 = nil + 3.times do |i| + resp2 = @client.make_request(:put, "/api/v2/video/calltypes/#{ct_name}", body: { + settings: { + audio: { default_device: 'earpiece', mic_default_on: false }, + recording: { mode: 'disabled' }, + backstage: { enabled: true } + }, + grants: { + 'host' => %w[join-backstage] + } + }) + break + rescue GetStreamRuby::APIError => e + raise if i == 2 + + sleep(5) + end + expect(resp2).not_to be_nil + + # Read call type (with retry) + resp3 = nil + 3.times do |i| + resp3 = @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") + break + rescue GetStreamRuby::APIError => e + raise if i == 2 + + sleep(5) + end + expect(resp3.name).to eq(ct_name) + + # Delete call type (with retry for eventual consistency) + sleep(6) + 3.times do |i| + @client.make_request(:delete, "/api/v2/video/calltypes/#{ct_name}") + @created_call_type_names.delete(ct_name) + break + rescue GetStreamRuby::APIError => e + raise if i == 2 + + sleep(5) + end + end + end + + # --------------------------------------------------------------------------- + # CreateCallWithMembers + # --------------------------------------------------------------------------- + + describe 'CreateCallWithMembers' do + it 'creates a call and adds members' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + resp = create_call('default', call_id, { + data: { + created_by_id: @user1, + members: [ + { user_id: @user1 }, + { user_id: @user2 } + ] + } + }) + expect(resp).not_to be_nil + call_h = resp.to_h + expect(call_h['call']).not_to be_nil + end + end + + # --------------------------------------------------------------------------- + # BlockUnblockUserFromCalls + # --------------------------------------------------------------------------- + + describe 'BlockUnblockUserFromCalls' do + it 'blocks a user from a call and unblocks' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + # Block user + @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/block", + body: { user_id: @user2 } + ) + + # Verify blocked + resp = get_call('default', call_id) + call_h = resp.to_h + blocked_ids = call_h.dig('call', 'blocked_user_ids') || [] + expect(blocked_ids).to include(@user2) + + # Unblock user + @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/unblock", + body: { user_id: @user2 } + ) + + # Verify unblocked (with retry for eventual consistency) + unblocked = false + 6.times do |i| + sleep(2) + resp2 = get_call('default', call_id) + call_h2 = resp2.to_h + blocked_ids2 = call_h2.dig('call', 'blocked_user_ids') || [] + unless blocked_ids2.include?(@user2) + unblocked = true + break + end + end + expect(unblocked).to be(true), 'Expected user to be unblocked after unblock call' + end + end + + # --------------------------------------------------------------------------- + # SendCustomEvent + # --------------------------------------------------------------------------- + + describe 'SendCustomEvent' do + it 'sends a custom event in a call' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + resp = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/event", + body: { user_id: @user1, custom: { bananas: 'good' } } + ) + expect(resp).not_to be_nil + end + end + + # --------------------------------------------------------------------------- + # MuteAll + # --------------------------------------------------------------------------- + + describe 'MuteAll' do + it 'mutes all users in a call' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + resp = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/mute_users", + body: { + muted_by_id: @user1, + mute_all_users: true, + audio: true + } + ) + expect(resp).not_to be_nil + end + end + + # --------------------------------------------------------------------------- + # MuteSomeUsers + # --------------------------------------------------------------------------- + + describe 'MuteSomeUsers' do + it 'mutes specific users with audio, video, screenshare' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + resp = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/mute_users", + body: { + muted_by_id: @user1, + user_ids: [@user2, @user3], + audio: true, + video: true, + screenshare: true, + screenshare_audio: true + } + ) + expect(resp).not_to be_nil + end + end + + # --------------------------------------------------------------------------- + # UpdateUserPermissions + # --------------------------------------------------------------------------- + + describe 'UpdateUserPermissions' do + it 'revokes and grants permissions in a call' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + # Revoke send-audio + resp1 = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/user_permissions", + body: { + user_id: @user2, + revoke_permissions: ['send-audio'] + } + ) + expect(resp1).not_to be_nil + + # Grant send-audio back + resp2 = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/user_permissions", + body: { + user_id: @user2, + grant_permissions: ['send-audio'] + } + ) + expect(resp2).not_to be_nil + end + end + + # --------------------------------------------------------------------------- + # DeactivateUser (video context: deactivate/reactivate/batch) + # --------------------------------------------------------------------------- + + describe 'DeactivateUser' do + it 'deactivates, reactivates, and batch deactivates users' do + user_ids, _resp = create_test_users(2) + alice = user_ids[0] + bob = user_ids[1] + + # Deactivate single user + @client.common.deactivate_user( + alice, + GetStream::Generated::Models::DeactivateUserRequest.new + ) + + # Reactivate single user + @client.common.reactivate_user( + alice, + GetStream::Generated::Models::ReactivateUserRequest.new + ) + + # Batch deactivate + resp = @client.common.deactivate_users( + GetStream::Generated::Models::DeactivateUsersRequest.new(user_ids: [alice, bob]) + ) + expect(resp.task_id).not_to be_nil + + task_result = wait_for_task(resp.task_id) + expect(task_result.status).to eq('completed') + end + end + + # --------------------------------------------------------------------------- + # CreateCallWithSessionTimer + # --------------------------------------------------------------------------- + + describe 'CreateCallWithSessionTimer' do + it 'creates a call with max_duration_seconds and updates it' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + resp = create_call('default', call_id, { + data: { + created_by_id: @user1, + settings_override: { + limits: { max_duration_seconds: 3600 } + } + } + }) + call_h = resp.to_h + max_dur = call_h.dig('call', 'settings', 'limits', 'max_duration_seconds') + expect(max_dur).to eq(3600) + + # Update to 7200 + resp2 = update_call('default', call_id, { + settings_override: { + limits: { max_duration_seconds: 7200 } + } + }) + call_h2 = resp2.to_h + max_dur2 = call_h2.dig('call', 'settings', 'limits', 'max_duration_seconds') + expect(max_dur2).to eq(7200) + + # Reset to 0 + resp3 = update_call('default', call_id, { + settings_override: { + limits: { max_duration_seconds: 0 } + } + }) + call_h3 = resp3.to_h + max_dur3 = call_h3.dig('call', 'settings', 'limits', 'max_duration_seconds') + expect(max_dur3).to eq(0) + end + end + + # --------------------------------------------------------------------------- + # UserBlocking (app-level user block/unblock, not call-level) + # --------------------------------------------------------------------------- + + describe 'UserBlocking' do + it 'blocks and unblocks a user at app level' do + user_ids, _resp = create_test_users(2) + alice = user_ids[0] + bob = user_ids[1] + + # Block + @client.common.block_users( + GetStream::Generated::Models::BlockUsersRequest.new( + blocked_user_id: bob, + user_id: alice + ) + ) + + # Verify blocked + resp = @client.common.get_blocked_users(alice) + blocks = resp.blocks || [] + expect(blocks.length).to be >= 1 + block_h = blocks[0].is_a?(Hash) ? blocks[0] : blocks[0].to_h + expect(block_h['blocked_user_id']).to eq(bob) + + # Unblock + @client.common.unblock_users( + GetStream::Generated::Models::UnblockUsersRequest.new( + blocked_user_id: bob, + user_id: alice + ) + ) + + # Verify unblocked + resp2 = @client.common.get_blocked_users(alice) + blocks2 = resp2.blocks || [] + blocked_ids = blocks2.map do |b| + h = b.is_a?(Hash) ? b : b.to_h + h['blocked_user_id'] + end + expect(blocked_ids).not_to include(bob) + end + end + + # --------------------------------------------------------------------------- + # CreateCallWithBackstageAndJoinAhead + # --------------------------------------------------------------------------- + + describe 'CreateCallWithBackstageAndJoinAhead' do + it 'creates a call with backstage and join_ahead_time_seconds' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + starts_at = (Time.now.utc + 30 * 60).strftime('%Y-%m-%dT%H:%M:%S.%NZ') + + resp = create_call('default', call_id, { + data: { + starts_at: starts_at, + created_by_id: @user1, + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 300 } + } + } + }) + call_h = resp.to_h + join_ahead = call_h.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') + expect(join_ahead).to eq(300) + + # Update to 600 + resp2 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 600 } + } + }) + call_h2 = resp2.to_h + join_ahead2 = call_h2.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') + expect(join_ahead2).to eq(600) + + # Reset to 0 + resp3 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 0 } + } + }) + call_h3 = resp3.to_h + join_ahead3 = call_h3.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') + expect(join_ahead3).to eq(0) + end + end + + # --------------------------------------------------------------------------- + # DeleteCall (soft) + # --------------------------------------------------------------------------- + + describe 'DeleteCall (soft)' do + it 'soft deletes a call and verifies not found' do + call_id = new_call_id + # Don't add to @created_call_ids since we're deleting it here + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + resp = delete_call_req('default', call_id, {}) + resp_h = resp.to_h + expect(resp_h['call']).not_to be_nil + # task_id should be nil for soft delete + expect(resp_h['task_id']).to be_nil + + # Verify not found (with retry for eventual consistency) + sleep(3) + found = false + 3.times do |i| + begin + get_call('default', call_id) + sleep(3) # still found, wait and retry + rescue GetStreamRuby::APIError => e + found = true if e.message.include?("Can't find call with id") + break + end + end + expect(found).to be(true), 'Expected call to be not found after soft delete' + end + end + + # --------------------------------------------------------------------------- + # HardDeleteCall + # --------------------------------------------------------------------------- + + describe 'HardDeleteCall' do + it 'hard deletes a call with task polling' do + call_id = new_call_id + # Don't add to @created_call_ids since we're deleting it here + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + resp = delete_call_req('default', call_id, { hard: true }) + resp_h = resp.to_h + task_id = resp_h['task_id'] + expect(task_id).not_to be_nil + + task_result = wait_for_task(task_id) + expect(task_result.status).to eq('completed') + + # Verify not found (with retry for eventual consistency) + sleep(3) + found = false + 3.times do |i| + begin + get_call('default', call_id) + sleep(3) # still found, wait and retry + rescue GetStreamRuby::APIError => e + found = true if e.message.include?("Can't find call with id") + break + end + end + expect(found).to be(true), 'Expected call to be not found after hard delete' + end + end + + # --------------------------------------------------------------------------- + # Teams + # --------------------------------------------------------------------------- + + describe 'Teams' do + it 'creates a user with teams, creates a call with team, queries' do + team_user_id = "test-user-#{SecureRandom.uuid}" + @created_user_ids << team_user_id + + @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + team_user_id => GetStream::Generated::Models::UserRequest.new( + id: team_user_id, + name: 'Team User', + role: 'user', + teams: %w[red blue] + ) + } + ) + ) + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + resp = create_call('default', call_id, { + data: { + created_by_id: team_user_id, + team: 'blue' + } + }) + call_h = resp.to_h + expect(call_h.dig('call', 'team')).to eq('blue') + + # Query calls by team + query_resp = @client.make_request(:post, '/api/v2/video/calls', body: { + filter_conditions: { + 'id' => call_id, + 'team' => { '$eq' => 'blue' } + } + }) + query_h = query_resp.to_h + expect(query_h['calls'].length).to be >= 1 + end + end + + # --------------------------------------------------------------------------- + # ExternalStorageOperations + # --------------------------------------------------------------------------- + + describe 'ExternalStorageOperations' do + it 'creates, lists, and deletes external storage' do + storage_name = "test-storage-#{random_string(10)}" + + # Create external storage + create_resp = @client.make_request(:post, '/api/v2/external_storage', body: { + bucket: 'test-bucket', + name: storage_name, + storage_type: 's3', + path: 'test-directory/', + aws_s3: { + s3_region: 'us-east-1', + s3_api_key: 'test-access-key', + s3_secret: 'test-secret' + } + }) + expect(create_resp).not_to be_nil + + # Verify via list (with retry for eventual consistency) + found = false + 8.times do |i| + sleep(3) + list_resp = @client.make_request(:get, '/api/v2/external_storage') + storages_h = list_resp.to_h['external_storages'] || {} + if storages_h.key?(storage_name) + found = true + break + end + end + expect(found).to be(true), "Expected storage #{storage_name} to appear in list" + + # Delete external storage (with retry for eventual consistency) + 5.times do |i| + @client.make_request(:delete, "/api/v2/external_storage/#{storage_name}") + break + rescue GetStreamRuby::APIError => e + raise if i == 4 + + sleep(3) + end + end + end + + # --------------------------------------------------------------------------- + # EnableCallRecordingAndBackstageMode + # --------------------------------------------------------------------------- + + describe 'EnableCallRecordingAndBackstageMode' do + it 'updates call settings for recording and backstage' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + # Enable recording + resp1 = update_call('default', call_id, { + settings_override: { + recording: { mode: 'available', audio_only: true } + } + }) + call_h1 = resp1.to_h + expect(call_h1.dig('call', 'settings', 'recording', 'mode')).to eq('available') + + # Enable backstage + resp2 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true } + } + }) + call_h2 = resp2.to_h + expect(call_h2.dig('call', 'settings', 'backstage', 'enabled')).to eq(true) + end + end + + # --------------------------------------------------------------------------- + # DeleteRecordingsAndTranscriptions + # --------------------------------------------------------------------------- + + describe 'DeleteRecordingsAndTranscriptions' do + it 'returns error when deleting non-existent recording' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + expect do + @client.make_request( + :delete, + "/api/v2/video/call/default/#{call_id}/non-existent-session/recordings/non-existent-filename" + ) + end.to raise_error(GetStreamRuby::APIError) + end + + it 'returns error when deleting non-existent transcription' do + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user1 } + }) + + expect do + @client.make_request( + :delete, + "/api/v2/video/call/default/#{call_id}/non-existent-session/transcriptions/non-existent-filename" + ) + end.to raise_error(GetStreamRuby::APIError) + end + end +end From ce5af333a521e52ecb75e4205d40c070c81c8203 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 14:04:14 +0100 Subject: [PATCH 07/29] test: blocklist deletion cleanup --- spec/integration/chat_message_integration_spec.rb | 6 +++--- spec/integration/chat_misc_integration_spec.rb | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/integration/chat_message_integration_spec.rb b/spec/integration/chat_message_integration_spec.rb index ec3152d..4545e2f 100644 --- a/spec/integration/chat_message_integration_spec.rb +++ b/spec/integration/chat_message_integration_spec.rb @@ -529,7 +529,7 @@ def undelete_message(message_id, body) query: 'test', message_filter_conditions: { 'text' => { '$q' => 'test' } } ) - end.to raise_error(StandardError) + end.to raise_error(GetStreamRuby::APIError) end end @@ -545,7 +545,7 @@ def undelete_message(message_id, body) ) # If no error, the API accepts the combination — verify a valid response expect(resp).not_to be_nil - rescue StandardError + rescue GetStreamRuby::APIError # Expected error — test passes end end @@ -560,7 +560,7 @@ def undelete_message(message_id, body) offset: 1, next: SecureRandom.hex(5) ) - end.to raise_error(StandardError) + end.to raise_error(GetStreamRuby::APIError) end end diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb index 825d1c8..74bd959 100644 --- a/spec/integration/chat_misc_integration_spec.rb +++ b/spec/integration/chat_misc_integration_spec.rb @@ -153,7 +153,9 @@ words: %w[word1] ) ) + @created_blocklist_names << del_name @client.common.delete_block_list(del_name) + @created_blocklist_names.delete(del_name) end end From f50f44d41704678e8b376acb307f2aa68fc76db4 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 14:09:37 +0100 Subject: [PATCH 08/29] test: minor tweaks --- .../integration/chat_channel_integration_spec.rb | 2 +- spec/integration/chat_test_helpers.rb | 8 ++++---- spec/integration/chat_user_integration_spec.rb | 2 +- spec/integration/video_integration_spec.rb | 16 +++++++++++----- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/spec/integration/chat_channel_integration_spec.rb b/spec/integration/chat_channel_integration_spec.rb index e7b3b1e..026b288 100644 --- a/spec/integration/chat_channel_integration_spec.rb +++ b/spec/integration/chat_channel_integration_spec.rb @@ -650,7 +650,7 @@ def delete_channel_image(type, id, url) channel_h = q_resp.channels.first.to_h.dig('channel') || {} msg_count = channel_h['message_count'] # message_count may be nil if disabled on channel type - expect(msg_count).to be >= 1 if msg_count + expect(msg_count).to be_nil.or be >= 1 end end diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb index 3690d65..fdda42f 100644 --- a/spec/integration/chat_test_helpers.rb +++ b/spec/integration/chat_test_helpers.rb @@ -140,14 +140,14 @@ def delete_users_with_retry(user_ids) # Helper 7: wait_for_task # --------------------------------------------------------------------------- - def wait_for_task(task_id) - 30.times do + def wait_for_task(task_id, max_attempts: 30, interval_seconds: 1) + max_attempts.times do result = @client.common.get_task(task_id) return result if %w[completed failed].include?(result.status) - sleep(1) + sleep(interval_seconds) end - raise "Task #{task_id} did not complete after 30 attempts" + raise "Task #{task_id} did not complete after #{max_attempts} attempts" end # --------------------------------------------------------------------------- diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb index 6cac704..449c747 100644 --- a/spec/integration/chat_user_integration_spec.rb +++ b/spec/integration/chat_user_integration_spec.rb @@ -196,7 +196,7 @@ def query_users_with_filter(filter, **opts) rescue GetStreamRuby::APIError => e raise unless e.message.include?('Too many requests') - sleep((i + 1) * 3) + sleep([2**i, 30].min) end expect(resp).not_to be_nil diff --git a/spec/integration/video_integration_spec.rb b/spec/integration/video_integration_spec.rb index 7b74d72..1360a3c 100644 --- a/spec/integration/video_integration_spec.rb +++ b/spec/integration/video_integration_spec.rb @@ -103,8 +103,14 @@ def new_call_type_name }) expect(resp.name).to eq(ct_name) - # Wait for eventual consistency (video call types need longer propagation than chat channel types) - sleep(20) + # Wait for eventual consistency (video call types need longer propagation than chat channel types). + # Poll for availability instead of a fixed sleep to keep the suite fast. + 20.times do + @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") + break + rescue GetStreamRuby::APIError + sleep(2) + end # Update call type settings (with retry for eventual consistency) resp2 = nil @@ -120,7 +126,7 @@ def new_call_type_name } }) break - rescue GetStreamRuby::APIError => e + rescue GetStreamRuby::APIError raise if i == 2 sleep(5) @@ -132,7 +138,7 @@ def new_call_type_name 3.times do |i| resp3 = @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") break - rescue GetStreamRuby::APIError => e + rescue GetStreamRuby::APIError raise if i == 2 sleep(5) @@ -632,7 +638,7 @@ def new_call_type_name it 'creates, lists, and deletes external storage' do storage_name = "test-storage-#{random_string(10)}" - # Create external storage + # Create external storage (fake credentials for API contract testing only) create_resp = @client.make_request(:post, '/api/v2/external_storage', body: { bucket: 'test-bucket', name: storage_name, From 0be572dead7ce3ec74ccb77eb23e14210e7a63d3 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 15:22:31 +0100 Subject: [PATCH 09/29] test: case fixes and performance improvement --- .../chat_message_integration_spec.rb | 2 +- .../integration/chat_misc_integration_spec.rb | 13 ++- spec/integration/chat_test_helpers.rb | 2 +- spec/integration/feed_integration_spec.rb | 93 ++++++++++++------- spec/integration/video_integration_spec.rb | 40 ++++---- 5 files changed, 83 insertions(+), 67 deletions(-) diff --git a/spec/integration/chat_message_integration_spec.rb b/spec/integration/chat_message_integration_spec.rb index 4545e2f..b332910 100644 --- a/spec/integration/chat_message_integration_spec.rb +++ b/spec/integration/chat_message_integration_spec.rb @@ -369,7 +369,7 @@ def undelete_message(message_id, body) expect(attachments).to be_empty # Verify via GetMessage that attachments remain empty - sleep(3) + sleep(1) get_resp = get_message(send_resp.message.id) attachments2 = get_resp.message.to_h['attachments'] || [] expect(attachments2).to be_empty diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb index 74bd959..be6adef 100644 --- a/spec/integration/chat_misc_integration_spec.rb +++ b/spec/integration/chat_misc_integration_spec.rb @@ -38,19 +38,18 @@ # Clean up channel types (with retry due to eventual consistency) @created_channel_type_names&.each do |name| - 5.times do |i| + 3.times do |i| @client.make_request(:delete, "/api/v2/chat/channeltypes/#{name}") break rescue StandardError => e puts "Warning: Failed to delete channel type #{name} (attempt #{i + 1}): #{e.message}" - sleep(2) + sleep(1) end end # Clean up roles @created_role_names&.each do |name| - sleep(2) - 5.times do |i| + 3.times do |i| @client.common.delete_role(name) break rescue StandardError => e @@ -228,7 +227,7 @@ @created_channel_type_names << type_name # Wait for eventual consistency - sleep(6) + sleep(2) # Get channel type get_resp = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") @@ -253,7 +252,7 @@ }) @created_channel_type_names << del_name - sleep(6) + sleep(2) delete_err = nil 5.times do |i| @@ -264,7 +263,7 @@ break rescue StandardError => e delete_err = e - sleep(1) + sleep(2) end end expect(delete_err).to be_nil, "Channel type deletion should succeed: #{delete_err&.message}" diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb index fdda42f..5943979 100644 --- a/spec/integration/chat_test_helpers.rb +++ b/spec/integration/chat_test_helpers.rb @@ -132,7 +132,7 @@ def delete_users_with_retry(user_ids) rescue GetStreamRuby::APIError => e return unless e.message.include?('Too many requests') - sleep((i + 1) * 3) + sleep([2**i, 16].min) end end diff --git a/spec/integration/feed_integration_spec.rb b/spec/integration/feed_integration_spec.rb index 05b2ea0..4bbeab1 100644 --- a/spec/integration/feed_integration_spec.rb +++ b/spec/integration/feed_integration_spec.rb @@ -310,19 +310,20 @@ expect(response).to be_a(GetStreamRuby::StreamResponse) puts "✅ Created/updated users in batch: #{user_id_1}, #{user_id_2}" # snippet-stop: UpdateUsers - - # Wait for backend propagation - test_helper.wait_for_backend_propagation(1) ensure - # Cleanup created users - begin + # Cleanup created users (with retry for rate limits) + 3.times do |i| + delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( user_ids: [user_id_1, user_id_2], user: 'hard', ) client.common.delete_users(delete_request) + break rescue StandardError => e - puts "⚠️ Cleanup error: #{e.message}" + puts "⚠️ Cleanup error: #{e.message}" if i == 2 + sleep(2**i) + end end @@ -346,7 +347,6 @@ }, ) client.common.update_users(create_request) - test_helper.wait_for_backend_propagation(1) # snippet-start: UpdateUsersPartial # Partially update user @@ -366,15 +366,19 @@ puts "✅ Partially updated user: #{user_id}" # snippet-stop: UpdateUsersPartial ensure - # Cleanup - begin + # Cleanup (with retry for rate limits) + 3.times do |i| + delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( user_ids: [user_id], user: 'hard', ) client.common.delete_users(delete_request) + break rescue StandardError => e - puts "⚠️ Cleanup error: #{e.message}" + puts "⚠️ Cleanup error: #{e.message}" if i == 2 + sleep(2**i) + end end @@ -406,16 +410,27 @@ create_request = GetStream::Generated::Models::UpdateUsersRequest.new(users: users_hash) client.common.update_users(create_request) - test_helper.wait_for_backend_propagation(1) # snippet-start: DeleteUsers - # Delete users in batch - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: user_ids, - user: 'hard', - ) + # Delete users in batch (with retry for rate limits) + response = nil + 10.times do |i| - response = client.common.delete_users(delete_request) + delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( + user_ids: user_ids, + user: 'hard', + ) + + response = client.common.delete_users(delete_request) + break + rescue GetStreamRuby::APIError => e + raise unless e.message.include?('Too many requests') + + sleep([2**i, 30].min) + + end + + expect(response).not_to be_nil expect(response).to be_a(GetStreamRuby::StreamResponse) puts "✅ Deleted #{user_ids.length} users in batch" # snippet-stop: DeleteUsers @@ -832,30 +847,36 @@ puts "\n📤 Testing file upload..." - # Get the path to the test file (in the same directory as the spec) - test_file_path = File.join(__dir__, 'upload-test.png') - raise "Test file not found: #{test_file_path}" unless File.exist?(test_file_path) + # Create a temporary text file (feed API upload_file supports text, not images) + require 'tempfile' + tmpfile = Tempfile.new(['feed-upload-test-', '.txt']) + tmpfile.write('hello world test file content from Ruby SDK') + tmpfile.close - # Create file upload request - file_upload_request = GetStream::Generated::Models::FileUploadRequest.new( - file: test_file_path, - user: GetStream::Generated::Models::OnlyUserID.new(id: test_user_id_1), - ) + begin + # Create file upload request + file_upload_request = GetStream::Generated::Models::FileUploadRequest.new( + file: tmpfile.path, + user: GetStream::Generated::Models::OnlyUserID.new(id: test_user_id_1), + ) - # Upload the file - upload_response = client.common.upload_file(file_upload_request) + # Upload the file + upload_response = client.common.upload_file(file_upload_request) - expect(upload_response).to be_a(GetStreamRuby::StreamResponse) - expect(upload_response.file).not_to be_nil - expect(upload_response.file).to be_a(String) - expect(upload_response.file).not_to be_empty + expect(upload_response).to be_a(GetStreamRuby::StreamResponse) + expect(upload_response.file).not_to be_nil + expect(upload_response.file).to be_a(String) + expect(upload_response.file).not_to be_empty - puts '✅ File uploaded successfully' - puts " File URL: #{upload_response.file}" - puts " Thumbnail URL: #{upload_response.thumb_url}" if upload_response.thumb_url + puts '✅ File uploaded successfully' + puts " File URL: #{upload_response.file}" + puts " Thumbnail URL: #{upload_response.thumb_url}" if upload_response.thumb_url - # Verify the URL is a valid URL - expect(upload_response.file).to match(/^https?:\/\//) + # Verify the URL is a valid URL + expect(upload_response.file).to match(/^https?:\/\//) + ensure + tmpfile.unlink + end end diff --git a/spec/integration/video_integration_spec.rb b/spec/integration/video_integration_spec.rb index 1360a3c..804d236 100644 --- a/spec/integration/video_integration_spec.rb +++ b/spec/integration/video_integration_spec.rb @@ -33,7 +33,6 @@ # Clean up call types @created_call_type_names&.each do |name| - sleep(1) # small delay to avoid rate limits @client.make_request(:delete, "/api/v2/video/calltypes/#{name}") rescue StandardError => e puts "Warning: Failed to delete call type #{name}: #{e.message}" @@ -103,13 +102,12 @@ def new_call_type_name }) expect(resp.name).to eq(ct_name) - # Wait for eventual consistency (video call types need longer propagation than chat channel types). - # Poll for availability instead of a fixed sleep to keep the suite fast. - 20.times do + # Poll for eventual consistency + 10.times do @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") break rescue GetStreamRuby::APIError - sleep(2) + sleep(1) end # Update call type settings (with retry for eventual consistency) @@ -129,7 +127,7 @@ def new_call_type_name rescue GetStreamRuby::APIError raise if i == 2 - sleep(5) + sleep(2) end expect(resp2).not_to be_nil @@ -141,20 +139,20 @@ def new_call_type_name rescue GetStreamRuby::APIError raise if i == 2 - sleep(5) + sleep(2) end expect(resp3.name).to eq(ct_name) # Delete call type (with retry for eventual consistency) - sleep(6) - 3.times do |i| + sleep(2) + 5.times do |i| @client.make_request(:delete, "/api/v2/video/calltypes/#{ct_name}") @created_call_type_names.delete(ct_name) break rescue GetStreamRuby::APIError => e - raise if i == 2 + raise if i == 4 - sleep(5) + sleep(2) end end end @@ -218,8 +216,8 @@ def new_call_type_name # Verify unblocked (with retry for eventual consistency) unblocked = false - 6.times do |i| - sleep(2) + 5.times do + sleep(1) resp2 = get_call('default', call_id) call_h2 = resp2.to_h blocked_ids2 = call_h2.dig('call', 'blocked_user_ids') || [] @@ -532,12 +530,11 @@ def new_call_type_name expect(resp_h['task_id']).to be_nil # Verify not found (with retry for eventual consistency) - sleep(3) found = false - 3.times do |i| + 5.times do + sleep(1) begin get_call('default', call_id) - sleep(3) # still found, wait and retry rescue GetStreamRuby::APIError => e found = true if e.message.include?("Can't find call with id") break @@ -569,12 +566,11 @@ def new_call_type_name expect(task_result.status).to eq('completed') # Verify not found (with retry for eventual consistency) - sleep(3) found = false - 3.times do |i| + 5.times do + sleep(1) begin get_call('default', call_id) - sleep(3) # still found, wait and retry rescue GetStreamRuby::APIError => e found = true if e.message.include?("Can't find call with id") break @@ -654,8 +650,8 @@ def new_call_type_name # Verify via list (with retry for eventual consistency) found = false - 8.times do |i| - sleep(3) + 10.times do + sleep(1) list_resp = @client.make_request(:get, '/api/v2/external_storage') storages_h = list_resp.to_h['external_storages'] || {} if storages_h.key?(storage_name) @@ -672,7 +668,7 @@ def new_call_type_name rescue GetStreamRuby::APIError => e raise if i == 4 - sleep(3) + sleep(2) end end end From 22d9cf02c3a751fbb4f947d6bb22f8bfdcd3a5e8 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 16:02:49 +0100 Subject: [PATCH 10/29] test: improve loop time --- spec/integration/chat_user_integration_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb index 449c747..306d7a0 100644 --- a/spec/integration/chat_user_integration_spec.rb +++ b/spec/integration/chat_user_integration_spec.rb @@ -182,8 +182,11 @@ def query_users_with_filter(filter, **opts) # Remove from tracked list so cleanup doesn't double-delete user_ids.each { |uid| @created_user_ids.delete(uid) } + # delete_users is heavily rate-limited; previous spec cleanups may have + # exhausted the budget. Use fewer retries with longer waits to avoid + # wasting rate-limit tokens on rapid 429 responses. resp = nil - 10.times do |i| + 6.times do |i| resp = @client.common.delete_users( GetStream::Generated::Models::DeleteUsersRequest.new( user_ids: user_ids, @@ -196,7 +199,7 @@ def query_users_with_filter(filter, **opts) rescue GetStreamRuby::APIError => e raise unless e.message.include?('Too many requests') - sleep([2**i, 30].min) + sleep([5 * 2**i, 60].min) end expect(resp).not_to be_nil From b9da3edb42e881e89a73464b543f1b0abc0299e3 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 16:38:46 +0100 Subject: [PATCH 11/29] style: fix format issues --- .../chat_channel_integration_spec.rb | 360 +++++++++---- .../chat_message_integration_spec.rb | 321 ++++++++---- .../integration/chat_misc_integration_spec.rb | 444 ++++++++++------ .../chat_moderation_integration_spec.rb | 168 +++--- .../chat_polls_integration_spec.rb | 52 +- .../chat_reaction_integration_spec.rb | 70 ++- spec/integration/chat_test_helpers.rb | 38 +- .../integration/chat_user_integration_spec.rb | 236 ++++++--- spec/integration/video_integration_spec.rb | 494 +++++++++++------- 9 files changed, 1421 insertions(+), 762 deletions(-) diff --git a/spec/integration/chat_channel_integration_spec.rb b/spec/integration/chat_channel_integration_spec.rb index 026b288..e37574a 100644 --- a/spec/integration/chat_channel_integration_spec.rb +++ b/spec/integration/chat_channel_integration_spec.rb @@ -7,20 +7,25 @@ require_relative 'chat_test_helpers' RSpec.describe 'Chat Channel Integration', type: :integration do + include ChatTestHelpers before(:all) do + init_chat_client # Create shared test users for all subtests @shared_user_ids, _resp = create_test_users(4) @creator_id = @shared_user_ids[0] - @member_id1 = @shared_user_ids[1] - @member_id2 = @shared_user_ids[2] - @member_id3 = @shared_user_ids[3] + @member_id_1 = @shared_user_ids[1] + @member_id_2 = @shared_user_ids[2] + @member_id_3 = @shared_user_ids[3] + end after(:all) do + cleanup_chat_resources + end # --------------------------------------------------------------------------- @@ -77,7 +82,7 @@ def update_member_partial(type, id, body) :patch, "/api/v2/chat/channels/#{type}/#{id}/member", query_params: { 'user_id' => user_id }, - body: body + body: body, ) end @@ -85,7 +90,7 @@ def query_members_api(payload) @client.make_request( :get, '/api/v2/chat/members', - query_params: { 'payload' => JSON.generate(payload) } + query_params: { 'payload' => JSON.generate(payload) }, ) end @@ -110,38 +115,48 @@ def delete_channel_image(type, id, url) # --------------------------------------------------------------------------- describe 'CreateChannelWithID' do + it 'creates channel and verifies via QueryChannels' do + _type, channel_id, _resp = create_test_channel(@creator_id) resp = query_channels( - filter_conditions: { 'id' => channel_id } + filter_conditions: { 'id' => channel_id }, ) expect(resp.channels).not_to be_nil expect(resp.channels).not_to be_empty ch = resp.channels.first.to_h expect(ch.dig('channel', 'id')).to eq(channel_id) expect(ch.dig('channel', 'type')).to eq('messaging') + end + end describe 'CreateChannelWithMembers' do + it 'creates channel with 3 members and verifies count' do + _type, channel_id, _resp = create_test_channel_with_members( @creator_id, - [@creator_id, @member_id1, @member_id2] + [@creator_id, @member_id_1, @member_id_2], ) resp = get_or_create_channel('messaging', channel_id) expect(resp.members).not_to be_nil expect(resp.members.length).to be >= 3 + end + end describe 'CreateDistinctChannel' do + it 'creates distinct channel and verifies same CID on second call' do + members = [ { user_id: @creator_id }, - { user_id: @member_id1 } + { user_id: @member_id_1 }, ] resp = @client.make_request( @@ -150,48 +165,56 @@ def delete_channel_image(type, id, url) body: { data: { created_by_id: @creator_id, - members: members - } - } + members: members, + }, + }, ) expect(resp.channel).not_to be_nil - cid1 = resp.channel.to_h['cid'] + cid_1 = resp.channel.to_h['cid'] # Call again with same members — should return same channel - resp2 = @client.make_request( + resp_2 = @client.make_request( :post, '/api/v2/chat/channels/messaging/query', body: { data: { created_by_id: @creator_id, - members: members - } - } + members: members, + }, + }, ) - cid2 = resp2.channel.to_h['cid'] - expect(cid1).to eq(cid2) + cid_2 = resp_2.channel.to_h['cid'] + expect(cid_1).to eq(cid_2) # Cleanup: hard delete ch_id = resp.channel.to_h['id'] @created_channel_cids << "messaging:#{ch_id}" unless @created_channel_cids.include?("messaging:#{ch_id}") + end + end describe 'QueryChannels' do + it 'creates channel and queries by type+id' do + _type, channel_id, _resp = create_test_channel(@creator_id) resp = query_channels( - filter_conditions: { 'type' => 'messaging', 'id' => channel_id } + filter_conditions: { 'type' => 'messaging', 'id' => channel_id }, ) expect(resp.channels).not_to be_nil expect(resp.channels).not_to be_empty expect(resp.channels.first.to_h.dig('channel', 'id')).to eq(channel_id) + end + end describe 'UpdateChannel' do + it 'updates with custom data and message, verifies custom field' do + _type, channel_id, _resp = create_test_channel(@creator_id) resp = update_channel('messaging', channel_id, @@ -201,11 +224,15 @@ def delete_channel_image(type, id, url) ch = resp.channel.to_h custom = ch['custom'] || {} expect(custom['color']).to eq('blue') + end + end describe 'PartialUpdateChannel' do + it 'sets fields then unsets one' do + _type, channel_id, _resp = create_test_channel(@creator_id) # Set fields @@ -217,16 +244,20 @@ def delete_channel_image(type, id, url) expect(custom['color']).to eq('red') # Unset fields - resp2 = update_channel_partial('messaging', channel_id, unset: ['color']) - expect(resp2.channel).not_to be_nil - ch2 = resp2.channel.to_h - custom2 = ch2['custom'] || {} - expect(custom2).not_to have_key('color') + resp_2 = update_channel_partial('messaging', channel_id, unset: ['color']) + expect(resp_2.channel).not_to be_nil + ch_2 = resp_2.channel.to_h + custom_2 = ch_2['custom'] || {} + expect(custom_2).not_to have_key('color') + end + end describe 'DeleteChannel' do + it 'soft deletes channel and verifies response' do + channel_id = "test-del-#{SecureRandom.hex(6)}" get_or_create_channel('messaging', channel_id, data: { created_by_id: @creator_id }) @@ -234,111 +265,135 @@ def delete_channel_image(type, id, url) resp = delete_channel('messaging', channel_id) expect(resp.channel).not_to be_nil + end + end describe 'HardDeleteChannels' do + it 'hard deletes 2 channels via batch and polls task' do - _type1, channel_id1, _resp1 = create_test_channel(@creator_id) - _type2, channel_id2, _resp2 = create_test_channel(@creator_id) - cid1 = "messaging:#{channel_id1}" - cid2 = "messaging:#{channel_id2}" + _type_1, channel_id_1, _resp_1 = create_test_channel(@creator_id) + _type_2, channel_id_2, _resp_2 = create_test_channel(@creator_id) + + cid_1 = "messaging:#{channel_id_1}" + cid_2 = "messaging:#{channel_id_2}" # Remove from tracked list since batch delete will handle it - @created_channel_cids.delete(cid1) - @created_channel_cids.delete(cid2) + @created_channel_cids.delete(cid_1) + @created_channel_cids.delete(cid_2) - resp = delete_channels_batch(cids: [cid1, cid2], hard_delete: true) + resp = delete_channels_batch(cids: [cid_1, cid_2], hard_delete: true) expect(resp.task_id).not_to be_nil result = wait_for_task(resp.task_id) expect(result.status).to eq('completed') + end + end describe 'AddRemoveMembers' do + it 'adds 2 members, verifies count; removes 1, verifies removed' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) # Add members update_channel('messaging', channel_id, - add_members: [{ user_id: @member_id2 }, { user_id: @member_id3 }]) + add_members: [{ user_id: @member_id_2 }, { user_id: @member_id_3 }]) # Verify members added resp = get_or_create_channel('messaging', channel_id) expect(resp.members.length).to be >= 4 # Remove a member - update_channel('messaging', channel_id, remove_members: [@member_id3]) + update_channel('messaging', channel_id, remove_members: [@member_id_3]) # Verify member removed - resp2 = get_or_create_channel('messaging', channel_id) - member_ids = resp2.members.map { |m| m.to_h['user_id'] || m.to_h.dig('user', 'id') } - expect(member_ids).not_to include(@member_id3) + resp_2 = get_or_create_channel('messaging', channel_id) + member_ids = resp_2.members.map { |m| m.to_h['user_id'] || m.to_h.dig('user', 'id') } + expect(member_ids).not_to include(@member_id_3) + end + end describe 'QueryMembers' do + it 'creates channel with 3 members and queries members' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1, @member_id2] + @creator_id, [@creator_id, @member_id_1, @member_id_2] ) resp = query_members_api( type: 'messaging', id: channel_id, - filter_conditions: {} + filter_conditions: {}, ) expect(resp.members).not_to be_nil expect(resp.members.length).to be >= 3 + end + end describe 'InviteAcceptReject' do + it 'creates channel with invites, accepts one, rejects one' do + channel_id = "test-inv-#{SecureRandom.hex(6)}" get_or_create_channel('messaging', channel_id, data: { created_by_id: @creator_id, members: [{ user_id: @creator_id }], - invites: [{ user_id: @member_id1 }, { user_id: @member_id2 }] + invites: [{ user_id: @member_id_1 }, { user_id: @member_id_2 }], }) @created_channel_cids << "messaging:#{channel_id}" # Accept invite update_channel('messaging', channel_id, accept_invite: true, - user_id: @member_id1) + user_id: @member_id_1) # Reject invite update_channel('messaging', channel_id, reject_invite: true, - user_id: @member_id2) + user_id: @member_id_2) + end + end describe 'HideShowChannel' do + it 'hides channel for user, then shows' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) # Hide - hide_channel('messaging', channel_id, user_id: @member_id1) + hide_channel('messaging', channel_id, user_id: @member_id_1) # Show - show_channel('messaging', channel_id, user_id: @member_id1) + show_channel('messaging', channel_id, user_id: @member_id_1) + end + end describe 'TruncateChannel' do + it 'sends 3 messages, truncates, verifies empty' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) send_test_message('messaging', channel_id, @creator_id, 'Message 1') @@ -350,11 +405,15 @@ def delete_channel_image(type, id, url) resp = get_or_create_channel('messaging', channel_id) messages = resp.messages || [] expect(messages).to be_empty + end + end describe 'FreezeUnfreezeChannel' do + it 'sets frozen=true, verifies; sets frozen=false, verifies' do + _type, channel_id, _resp = create_test_channel(@creator_id) # Freeze @@ -362,36 +421,44 @@ def delete_channel_image(type, id, url) expect(resp.channel.to_h['frozen']).to eq(true) # Unfreeze - resp2 = update_channel_partial('messaging', channel_id, set: { 'frozen' => false }) - expect(resp2.channel.to_h['frozen']).to eq(false) + resp_2 = update_channel_partial('messaging', channel_id, set: { 'frozen' => false }) + expect(resp_2.channel.to_h['frozen']).to eq(false) + end + end describe 'MarkReadUnread' do + it 'sends message, marks read, marks unread' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) msg_id = send_test_message('messaging', channel_id, @creator_id, 'Message to mark read') # Mark read - mark_read('messaging', channel_id, user_id: @member_id1) + mark_read('messaging', channel_id, user_id: @member_id_1) # Mark unread from this message - mark_unread('messaging', channel_id, user_id: @member_id1, message_id: msg_id) + mark_unread('messaging', channel_id, user_id: @member_id_1, message_id: msg_id) + end + end describe 'MuteUnmuteChannel' do + it 'mutes channel, verifies via query with muted=true; unmutes' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) cid = "messaging:#{channel_id}" # Mute - mute_resp = mute_channel(channel_cids: [cid], user_id: @member_id1) + mute_resp = mute_channel(channel_cids: [cid], user_id: @member_id_1) expect(mute_resp).not_to be_nil expect(mute_resp.channel_mute).not_to be_nil expect(mute_resp.channel_mute.to_h.dig('channel', 'cid')).to eq(cid) @@ -399,32 +466,36 @@ def delete_channel_image(type, id, url) # Verify via QueryChannels with muted=true q_resp = query_channels( filter_conditions: { 'muted' => true, 'cid' => cid }, - user_id: @member_id1 + user_id: @member_id_1, ) expect(q_resp.channels.length).to eq(1) expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) # Unmute - unmute_channel(channel_cids: [cid], user_id: @member_id1) + unmute_channel(channel_cids: [cid], user_id: @member_id_1) # Verify unmuted - q_resp2 = query_channels( + q_resp_2 = query_channels( filter_conditions: { 'muted' => false, 'cid' => cid }, - user_id: @member_id1 + user_id: @member_id_1, ) - expect(q_resp2.channels.length).to eq(1) + expect(q_resp_2.channels.length).to eq(1) + end + end describe 'MemberPartialUpdate' do + it 'sets custom fields on member; unsets one' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) # Set custom fields resp = update_member_partial('messaging', channel_id, - user_id: @member_id1, + user_id: @member_id_1, set: { 'role_label' => 'moderator', 'score' => 42 }) expect(resp.channel_member).not_to be_nil member_h = resp.channel_member.to_h @@ -432,73 +503,85 @@ def delete_channel_image(type, id, url) expect(custom['role_label']).to eq('moderator') # Unset a custom field - resp2 = update_member_partial('messaging', channel_id, - user_id: @member_id1, - unset: ['score']) - expect(resp2.channel_member).not_to be_nil - member_h2 = resp2.channel_member.to_h - custom2 = member_h2['custom'] || {} - expect(custom2).not_to have_key('score') + resp_2 = update_member_partial('messaging', channel_id, + user_id: @member_id_1, + unset: ['score']) + expect(resp_2.channel_member).not_to be_nil + member_h_2 = resp_2.channel_member.to_h + custom_2 = member_h_2['custom'] || {} + expect(custom_2).not_to have_key('score') + end + end describe 'AssignRoles' do + it 'assigns channel_moderator role, verifies via QueryMembers' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) # Assign role update_channel('messaging', channel_id, - assign_roles: [{ user_id: @member_id1, channel_role: 'channel_moderator' }]) + assign_roles: [{ user_id: @member_id_1, channel_role: 'channel_moderator' }]) # Verify via QueryMembers q_resp = query_members_api( type: 'messaging', id: channel_id, - filter_conditions: { 'id' => @member_id1 } + filter_conditions: { 'id' => @member_id_1 }, ) expect(q_resp.members).not_to be_empty expect(q_resp.members.first.to_h['channel_role']).to eq('channel_moderator') + end + end describe 'AddDemoteModerators' do + it 'adds moderator, verifies; demotes, verifies back to member' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) # Add moderator - update_channel('messaging', channel_id, add_moderators: [@member_id1]) + update_channel('messaging', channel_id, add_moderators: [@member_id_1]) # Verify role q_resp = query_members_api( type: 'messaging', id: channel_id, - filter_conditions: { 'id' => @member_id1 } + filter_conditions: { 'id' => @member_id_1 }, ) expect(q_resp.members).not_to be_empty expect(q_resp.members.first.to_h['channel_role']).to eq('channel_moderator') # Demote - update_channel('messaging', channel_id, demote_moderators: [@member_id1]) + update_channel('messaging', channel_id, demote_moderators: [@member_id_1]) # Verify back to member - q_resp2 = query_members_api( + q_resp_2 = query_members_api( type: 'messaging', id: channel_id, - filter_conditions: { 'id' => @member_id1 } + filter_conditions: { 'id' => @member_id_1 }, ) - expect(q_resp2.members).not_to be_empty - expect(q_resp2.members.first.to_h['channel_role']).to eq('channel_member') + expect(q_resp_2.members).not_to be_empty + expect(q_resp_2.members.first.to_h['channel_role']).to eq('channel_member') + end + end describe 'MarkUnreadWithThread' do + it 'creates thread and marks unread from thread' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) # Send parent message @@ -510,15 +593,19 @@ def delete_channel_image(type, id, url) # Mark unread from thread mark_unread('messaging', channel_id, - user_id: @member_id1, + user_id: @member_id_1, thread_id: parent_id) + end + end describe 'TruncateWithOptions' do + it 'truncates with message, skip_push, hard_delete' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) send_test_message('messaging', channel_id, @creator_id, 'Truncate msg 1') @@ -528,79 +615,91 @@ def delete_channel_image(type, id, url) message: { text: 'Channel was truncated', user_id: @creator_id }, skip_push: true, hard_delete: true) + end + end describe 'PinUnpinChannel' do + it 'pins channel, verifies via query; unpins, verifies' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) cid = "messaging:#{channel_id}" # Pin update_member_partial('messaging', channel_id, - user_id: @member_id1, + user_id: @member_id_1, set: { 'pinned' => true }) # Verify pinned q_resp = query_channels( filter_conditions: { 'pinned' => true, 'cid' => cid }, - user_id: @member_id1 + user_id: @member_id_1, ) expect(q_resp.channels.length).to eq(1) expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) # Unpin update_member_partial('messaging', channel_id, - user_id: @member_id1, + user_id: @member_id_1, set: { 'pinned' => false }) # Verify unpinned - q_resp2 = query_channels( + q_resp_2 = query_channels( filter_conditions: { 'pinned' => false, 'cid' => cid }, - user_id: @member_id1 + user_id: @member_id_1, ) - expect(q_resp2.channels.length).to eq(1) + expect(q_resp_2.channels.length).to eq(1) + end + end describe 'ArchiveUnarchiveChannel' do + it 'archives channel, verifies via query; unarchives, verifies' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) cid = "messaging:#{channel_id}" # Archive update_member_partial('messaging', channel_id, - user_id: @member_id1, + user_id: @member_id_1, set: { 'archived' => true }) # Verify archived q_resp = query_channels( filter_conditions: { 'archived' => true, 'cid' => cid }, - user_id: @member_id1 + user_id: @member_id_1, ) expect(q_resp.channels.length).to eq(1) expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) # Unarchive update_member_partial('messaging', channel_id, - user_id: @member_id1, + user_id: @member_id_1, set: { 'archived' => false }) # Verify unarchived - q_resp2 = query_channels( + q_resp_2 = query_channels( filter_conditions: { 'archived' => false, 'cid' => cid }, - user_id: @member_id1 + user_id: @member_id_1, ) - expect(q_resp2.channels.length).to eq(1) + expect(q_resp_2.channels.length).to eq(1) + end + end describe 'AddMembersWithRoles' do + it 'adds members with specific channel roles, verifies' do + _type, channel_id, _resp = create_test_channel(@creator_id) new_user_ids, _resp = create_test_users(2) @@ -611,62 +710,76 @@ def delete_channel_image(type, id, url) update_channel('messaging', channel_id, add_members: [ { user_id: mod_user_id, channel_role: 'channel_moderator' }, - { user_id: member_user_id, channel_role: 'channel_member' } + { user_id: member_user_id, channel_role: 'channel_member' }, ]) # Query to verify roles q_resp = query_members_api( type: 'messaging', id: channel_id, - filter_conditions: { 'id' => { '$in' => new_user_ids } } + filter_conditions: { 'id' => { '$in' => new_user_ids } }, ) role_map = {} q_resp.members.each do |m| + mh = m.to_h uid = mh['user_id'] || mh.dig('user', 'id') role_map[uid] = mh['channel_role'] + end expect(role_map[mod_user_id]).to eq('channel_moderator') expect(role_map[member_user_id]).to eq('channel_member') + end + end describe 'MessageCount' do + it 'sends message, queries channel, verifies message_count >= 1' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) send_test_message('messaging', channel_id, @creator_id, 'hello world') q_resp = query_channels( filter_conditions: { 'cid' => "messaging:#{channel_id}" }, - user_id: @creator_id + user_id: @creator_id, ) expect(q_resp.channels.length).to eq(1) - channel_h = q_resp.channels.first.to_h.dig('channel') || {} + channel_h = q_resp.channels.first.to_h['channel'] || {} msg_count = channel_h['message_count'] # message_count may be nil if disabled on channel type expect(msg_count).to be_nil.or be >= 1 + end + end describe 'SendChannelEvent' do + it 'sends typing.start event' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) send_event('messaging', channel_id, event: { type: 'typing.start', user_id: @creator_id }) + end + end describe 'FilterTags' do + it 'adds filter tags, removes filter tag' do + _type, channel_id, _resp = create_test_channel(@creator_id) # Add filter tags @@ -680,38 +793,46 @@ def delete_channel_image(type, id, url) # Remove a filter tag update_channel('messaging', channel_id, remove_filter_tags: ['sports']) + end + end describe 'MessageCountDisabled' do + it 'disables count_messages via config_overrides, verifies message_count nil' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) # Disable count_messages update_channel_partial('messaging', channel_id, set: { - 'config_overrides' => { 'count_messages' => false } + 'config_overrides' => { 'count_messages' => false }, }) send_test_message('messaging', channel_id, @creator_id, 'hello world disabled count') q_resp = query_channels( filter_conditions: { 'cid' => "messaging:#{channel_id}" }, - user_id: @creator_id + user_id: @creator_id, ) expect(q_resp.channels.length).to eq(1) - channel_h = q_resp.channels.first.to_h.dig('channel') || {} + channel_h = q_resp.channels.first.to_h['channel'] || {} expect(channel_h['message_count']).to be_nil + end + end describe 'MarkUnreadWithTimestamp' do + it 'sends message, gets timestamp, marks unread from timestamp' do + _type, channel_id, _resp = create_test_channel_with_members( - @creator_id, [@creator_id, @member_id1] + @creator_id, [@creator_id, @member_id_1] ) # Send message to get a valid timestamp @@ -729,13 +850,17 @@ def delete_channel_image(type, id, url) # Mark unread from timestamp mark_unread('messaging', channel_id, - user_id: @member_id1, + user_id: @member_id_1, message_timestamp: ts) + end + end describe 'HideForCreator' do + it 'creates channel with hide_for_creator=true, verifies hidden' do + channel_id = "test-hide-#{SecureRandom.hex(6)}" get_or_create_channel('messaging', channel_id, @@ -744,22 +869,26 @@ def delete_channel_image(type, id, url) created_by_id: @creator_id, members: [ { user_id: @creator_id }, - { user_id: @member_id1 } - ] + { user_id: @member_id_1 }, + ], }) @created_channel_cids << "messaging:#{channel_id}" # Channel should be hidden for creator q_resp = query_channels( filter_conditions: { 'cid' => "messaging:#{channel_id}" }, - user_id: @creator_id + user_id: @creator_id, ) expect(q_resp.channels).to be_empty + end + end describe 'UploadAndDeleteFile' do + it 'uploads a text file, verifies URL, deletes file' do + _type, channel_id, _resp = create_test_channel_with_members( @creator_id, [@creator_id] ) @@ -774,7 +903,7 @@ def delete_channel_image(type, id, url) 'messaging', channel_id, GetStream::Generated::Models::FileUploadRequest.new( file: tmpfile.path, - user: GetStream::Generated::Models::OnlyUserID.new(id: @creator_id) + user: GetStream::Generated::Models::OnlyUserID.new(id: @creator_id), ) ) expect(upload_resp.file).not_to be_nil @@ -786,11 +915,15 @@ def delete_channel_image(type, id, url) ensure tmpfile.unlink end + end + end describe 'UploadAndDeleteImage' do + it 'uploads an image file, verifies URL, deletes image' do + _type, channel_id, _resp = create_test_channel_with_members( @creator_id, [@creator_id] ) @@ -803,7 +936,7 @@ def delete_channel_image(type, id, url) 'messaging', channel_id, GetStream::Generated::Models::ImageUploadRequest.new( file: image_path, - user: GetStream::Generated::Models::OnlyUserID.new(id: @creator_id) + user: GetStream::Generated::Models::OnlyUserID.new(id: @creator_id), ) ) expect(upload_resp.file).not_to be_nil @@ -812,6 +945,9 @@ def delete_channel_image(type, id, url) # Delete image delete_channel_image('messaging', channel_id, image_url) + end + end + end diff --git a/spec/integration/chat_message_integration_spec.rb b/spec/integration/chat_message_integration_spec.rb index b332910..cbadd94 100644 --- a/spec/integration/chat_message_integration_spec.rb +++ b/spec/integration/chat_message_integration_spec.rb @@ -6,19 +6,24 @@ require_relative 'chat_test_helpers' RSpec.describe 'Chat Message Integration', type: :integration do + include ChatTestHelpers before(:all) do + init_chat_client # Create shared test users for all subtests @shared_user_ids, _resp = create_test_users(3) - @user1 = @shared_user_ids[0] - @user2 = @shared_user_ids[1] - @user3 = @shared_user_ids[2] + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @user_3 = @shared_user_ids[2] + end after(:all) do + cleanup_chat_resources + end # --------------------------------------------------------------------------- @@ -33,7 +38,7 @@ def get_many_messages(type, id, message_ids) @client.make_request( :get, "/api/v2/chat/channels/#{type}/#{id}/messages", - query_params: { 'ids' => message_ids.join(',') } + query_params: { 'ids' => message_ids.join(',') }, ) end @@ -86,12 +91,14 @@ def undelete_message(message_id, body) # --------------------------------------------------------------------------- describe 'SendAndGetMessage' do + it 'sends message, gets by ID, verifies text' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) msg_text = "Hello from integration test #{SecureRandom.hex(8)}" send_resp = send_msg('messaging', channel_id, - message: { text: msg_text, user_id: @user1 }) + message: { text: msg_text, user_id: @user_1 }) expect(send_resp.message).not_to be_nil msg_id = send_resp.message.id expect(msg_id).not_to be_nil @@ -102,83 +109,107 @@ def undelete_message(message_id, body) expect(get_resp.message).not_to be_nil expect(get_resp.message.to_h['id']).to eq(msg_id) expect(get_resp.message.to_h['text']).to eq(msg_text) + end + end describe 'GetManyMessages' do + it 'sends 3 messages, gets all 3 by IDs' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - id1 = send_test_message('messaging', channel_id, @user1, 'Msg 1') - id2 = send_test_message('messaging', channel_id, @user1, 'Msg 2') - id3 = send_test_message('messaging', channel_id, @user1, 'Msg 3') + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + id_1 = send_test_message('messaging', channel_id, @user_1, 'Msg 1') + id_2 = send_test_message('messaging', channel_id, @user_1, 'Msg 2') + id_3 = send_test_message('messaging', channel_id, @user_1, 'Msg 3') - resp = get_many_messages('messaging', channel_id, [id1, id2, id3]) + resp = get_many_messages('messaging', channel_id, [id_1, id_2, id_3]) expect(resp.messages).not_to be_nil expect(resp.messages.length).to eq(3) + end + end describe 'UpdateMessage' do + it 'sends message, updates text, verifies' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, 'Original text') + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Original text') updated_text = "Updated text #{SecureRandom.hex(8)}" - resp = update_message(msg_id, message: { text: updated_text, user_id: @user1 }) + resp = update_message(msg_id, message: { text: updated_text, user_id: @user_1 }) expect(resp.message).not_to be_nil expect(resp.message.to_h['text']).to eq(updated_text) + end + end describe 'PartialUpdateMessage' do + it 'sets custom fields; unsets one' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, 'Partial update test') + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Partial update test') # Set custom fields resp = update_message_partial(msg_id, set: { 'priority' => 'high', 'status' => 'reviewed' }, - user_id: @user1) + user_id: @user_1) expect(resp.message).not_to be_nil # Unset custom field - resp2 = update_message_partial(msg_id, - unset: ['status'], - user_id: @user1) - expect(resp2.message).not_to be_nil + resp_2 = update_message_partial(msg_id, + unset: ['status'], + user_id: @user_1) + expect(resp_2.message).not_to be_nil + end + end describe 'DeleteMessage' do + it 'soft deletes, verifies type=deleted' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, 'Message to delete') + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Message to delete') resp = delete_message(msg_id) expect(resp.message).not_to be_nil expect(resp.message.to_h['type']).to eq('deleted') + end + end describe 'HardDeleteMessage' do + it 'hard deletes, verifies type=deleted' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, 'Message to hard delete') + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Message to hard delete') resp = delete_message(msg_id, { 'hard' => 'true' }) expect(resp.message).not_to be_nil expect(resp.message.to_h['type']).to eq('deleted') + end + end describe 'PinUnpinMessage' do + it 'sends pinned message; unpins via partial update' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) # Send a pinned message send_resp = send_msg('messaging', channel_id, - message: { text: 'Pinned message', user_id: @user1, pinned: true }) + message: { text: 'Pinned message', user_id: @user_1, pinned: true }) expect(send_resp.message).not_to be_nil msg_id = send_resp.message.id expect(send_resp.message.to_h['pinned']).to eq(true) @@ -186,34 +217,42 @@ def undelete_message(message_id, body) # Unpin via partial update resp = update_message_partial(msg_id, set: { 'pinned' => false }, - user_id: @user1) + user_id: @user_1) expect(resp.message).not_to be_nil expect(resp.message.to_h['pinned']).to eq(false) + end + end describe 'TranslateMessage' do + it 'translates to Spanish, verifies i18n field' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, 'Hello, how are you?') + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Hello, how are you?') resp = translate_message(msg_id, language: 'es') expect(resp.message).not_to be_nil i18n = resp.message.to_h['i18n'] expect(i18n).not_to be_nil + end + end describe 'ThreadReply' do + it 'sends parent, sends reply with parent_id, gets replies' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) # Send parent message - parent_id = send_test_message('messaging', channel_id, @user1, 'Parent message for thread') + parent_id = send_test_message('messaging', channel_id, @user_1, 'Parent message for thread') # Send reply reply_resp = send_msg('messaging', channel_id, - message: { text: 'Reply to parent', user_id: @user2, parent_id: parent_id }) + message: { text: 'Reply to parent', user_id: @user_2, parent_id: parent_id }) expect(reply_resp.message).not_to be_nil expect(reply_resp.message.id).not_to be_nil @@ -221,46 +260,58 @@ def undelete_message(message_id, body) replies_resp = get_replies(parent_id) expect(replies_resp.messages).not_to be_nil expect(replies_resp.messages.length).to be >= 1 + end + end describe 'SearchMessages' do + it 'sends message with unique term, waits, searches, verifies found' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) search_term = "uniquesearch#{SecureRandom.hex(8)}" - send_test_message('messaging', channel_id, @user1, "This message contains #{search_term} for testing") + send_test_message('messaging', channel_id, @user_1, "This message contains #{search_term} for testing") # Wait for indexing sleep(2) resp = search_messages( query: search_term, - filter_conditions: { 'cid' => "messaging:#{channel_id}" } + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, ) expect(resp.results).not_to be_nil expect(resp.results).not_to be_empty + end + end describe 'SilentMessage' do + it 'sends with silent=true, verifies' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) resp = send_msg('messaging', channel_id, - message: { text: 'This is a silent message', user_id: @user1, silent: true }) + message: { text: 'This is a silent message', user_id: @user_1, silent: true }) expect(resp.message).not_to be_nil expect(resp.message.to_h['silent']).to eq(true) + end + end describe 'PendingMessage' do + it 'sends pending, commits, verifies (skip if not enabled)' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) begin send_resp = send_msg('messaging', channel_id, - message: { text: 'Pending message text', user_id: @user1 }, + message: { text: 'Pending message text', user_id: @user_1 }, pending: true, skip_push: true) rescue StandardError => e @@ -278,31 +329,35 @@ def undelete_message(message_id, body) commit_resp = commit_message(msg_id) expect(commit_resp.message).not_to be_nil expect(commit_resp.message.to_h['id']).to eq(msg_id) + end + end describe 'QueryMessageHistory' do + it 'sends, updates twice, queries history, verifies entries (skip if not enabled)' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) # Send initial message send_resp = send_msg('messaging', channel_id, - message: { text: 'initial text', user_id: @user1, + message: { text: 'initial text', user_id: @user_1, custom: { 'custom_field' => 'custom value' } }) msg_id = send_resp.message.id # Update by user1 - update_message(msg_id, message: { text: 'updated text', user_id: @user1, + update_message(msg_id, message: { text: 'updated text', user_id: @user_1, custom: { 'custom_field' => 'updated custom value' } }) # Update by user2 - update_message(msg_id, message: { text: 'updated text 2', user_id: @user2 }) + update_message(msg_id, message: { text: 'updated text 2', user_id: @user_2 }) # Query message history begin hist_resp = query_message_history( filter: { 'message_id' => msg_id }, - sort: [] + sort: [], ) rescue StandardError => e if e.message.include?('feature flag') || e.message.include?('not enabled') @@ -316,31 +371,37 @@ def undelete_message(message_id, body) # Verify history entries reference the correct message hist_resp.message_history.each do |entry| + h = entry.to_h expect(h['message_id']).to eq(msg_id) + end # Verify text values (descending by default: most recent first) expect(hist_resp.message_history[0].to_h['text']).to eq('updated text') expect(hist_resp.message_history[1].to_h['text']).to eq('initial text') + end + end describe 'QueryMessageHistorySort' do + it 'queries history with ascending sort' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) send_resp = send_msg('messaging', channel_id, - message: { text: 'sort initial', user_id: @user1 }) + message: { text: 'sort initial', user_id: @user_1 }) msg_id = send_resp.message.id - update_message(msg_id, message: { text: 'sort updated 1', user_id: @user1 }) - update_message(msg_id, message: { text: 'sort updated 2', user_id: @user1 }) + update_message(msg_id, message: { text: 'sort updated 1', user_id: @user_1 }) + update_message(msg_id, message: { text: 'sort updated 2', user_id: @user_1 }) begin hist_resp = query_message_history( filter: { 'message_id' => msg_id }, - sort: [{ 'field' => 'message_updated_at', 'direction' => 1 }] + sort: [{ 'field' => 'message_updated_at', 'direction' => 1 }], ) rescue StandardError => e if e.message.include?('feature flag') || e.message.include?('not enabled') @@ -354,15 +415,19 @@ def undelete_message(message_id, body) # Ascending: oldest first expect(hist_resp.message_history[0].to_h['text']).to eq('sort initial') + end + end describe 'SkipEnrichUrl' do + it 'sends with URL and skip_enrich_url=true, verifies no attachments' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) send_resp = send_msg('messaging', channel_id, - message: { text: 'Check out https://getstream.io for more info', user_id: @user1 }, + message: { text: 'Check out https://getstream.io for more info', user_id: @user_1 }, skip_enrich_url: true) expect(send_resp.message).not_to be_nil attachments = send_resp.message.to_h['attachments'] || [] @@ -371,37 +436,45 @@ def undelete_message(message_id, body) # Verify via GetMessage that attachments remain empty sleep(1) get_resp = get_message(send_resp.message.id) - attachments2 = get_resp.message.to_h['attachments'] || [] - expect(attachments2).to be_empty + attachments_2 = get_resp.message.to_h['attachments'] || [] + expect(attachments_2).to be_empty + end + end describe 'KeepChannelHidden' do + it 'hides channel, sends with keep_channel_hidden=true, verifies still hidden' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) cid = "messaging:#{channel_id}" # Hide the channel - hide_channel('messaging', channel_id, user_id: @user1) + hide_channel('messaging', channel_id, user_id: @user_1) # Send a message with keep_channel_hidden=true send_msg('messaging', channel_id, - message: { text: 'Hidden message', user_id: @user1 }, + message: { text: 'Hidden message', user_id: @user_1 }, keep_channel_hidden: true) # Query channels — the channel should still be hidden q_resp = query_channels( filter_conditions: { 'cid' => cid }, - user_id: @user1 + user_id: @user_1, ) expect(q_resp.channels).to be_empty + end + end describe 'UndeleteMessage' do + it 'soft deletes, undeletes, verifies restored' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, 'Message to undelete') + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Message to undelete') # Soft delete delete_message(msg_id) @@ -412,7 +485,7 @@ def undelete_message(message_id, body) # Undelete begin - undel_resp = undelete_message(msg_id, undeleted_by: @user1) + undel_resp = undelete_message(msg_id, undeleted_by: @user_1) rescue StandardError => e if e.message.include?('undeleted_by') || e.message.include?('required field') skip('UndeleteMessage requires undeleted_by field not yet in generated request struct') @@ -422,17 +495,21 @@ def undelete_message(message_id, body) expect(undel_resp.message).not_to be_nil expect(undel_resp.message.to_h['type']).not_to eq('deleted') expect(undel_resp.message.to_h['text']).to eq('Message to undelete') + end + end describe 'RestrictedVisibility' do + it 'sends with restricted_visibility list (skip if not enabled)' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) begin send_resp = send_msg('messaging', channel_id, - message: { text: 'Secret message', user_id: @user1, - restricted_visibility: [@user1] }) + message: { text: 'Secret message', user_id: @user_1, + restricted_visibility: [@user_1] }) rescue StandardError => e if e.message.include?('private messaging is not allowed') || e.message.include?('not enabled') skip('RestrictedVisibility (private messaging) is not enabled for this app') @@ -440,29 +517,37 @@ def undelete_message(message_id, body) raise end - expect(send_resp.message.to_h['restricted_visibility']).to eq([@user1]) + expect(send_resp.message.to_h['restricted_visibility']).to eq([@user_1]) + end + end describe 'DeleteMessageForMe' do + it 'deletes message with delete_for_me=true' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, 'test message to delete for me') - delete_message(msg_id, { 'delete_for_me' => 'true', 'deleted_by' => @user1 }) + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'test message to delete for me') + + delete_message(msg_id, { 'delete_for_me' => 'true', 'deleted_by' => @user_1 }) + end + end describe 'PinExpiration' do + it 'pins with 3s expiry, waits 4s, verifies expired' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) - msg_id = send_test_message('messaging', channel_id, @user2, 'Message to pin with expiry') + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + msg_id = send_test_message('messaging', channel_id, @user_2, 'Message to pin with expiry') # Pin with 3 second expiration expiry = (Time.now.utc + 3).strftime('%Y-%m-%dT%H:%M:%S.%6NZ') pin_resp = update_message_partial(msg_id, set: { 'pinned' => true, 'pin_expires' => expiry }, - user_id: @user1) + user_id: @user_1) expect(pin_resp.message).not_to be_nil expect(pin_resp.message.to_h['pinned']).to eq(true) @@ -472,100 +557,135 @@ def undelete_message(message_id, body) # Verify pin expired get_resp = get_message(msg_id) expect(get_resp.message.to_h['pinned']).to eq(false) + end + end describe 'SystemMessage' do + it 'sends with type=system, verifies' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) resp = send_msg('messaging', channel_id, - message: { text: 'User joined the channel', user_id: @user1, type: 'system' }) + message: { text: 'User joined the channel', user_id: @user_1, type: 'system' }) expect(resp.message).not_to be_nil expect(resp.message.to_h['type']).to eq('system') + end + end describe 'PendingFalse' do + it 'sends with pending=false, verifies immediately available' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) send_resp = send_msg('messaging', channel_id, - message: { text: 'Non-pending message', user_id: @user1 }, + message: { text: 'Non-pending message', user_id: @user_1 }, pending: false) expect(send_resp.message).not_to be_nil # Get the message to verify it's immediately available get_resp = get_message(send_resp.message.id) expect(get_resp.message.to_h['text']).to eq('Non-pending message') + end + end describe 'SearchWithMessageFilters' do + it 'searches using message_filter_conditions' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) search_term = "filterable#{SecureRandom.hex(8)}" - send_test_message('messaging', channel_id, @user1, "This has #{search_term} text") - send_test_message('messaging', channel_id, @user1, "This also has #{search_term} text") + send_test_message('messaging', channel_id, @user_1, "This has #{search_term} text") + send_test_message('messaging', channel_id, @user_1, "This also has #{search_term} text") # Wait for indexing sleep(2) resp = search_messages( filter_conditions: { 'cid' => "messaging:#{channel_id}" }, - message_filter_conditions: { 'text' => { '$q' => search_term } } + message_filter_conditions: { 'text' => { '$q' => search_term } }, ) expect(resp.results).not_to be_nil expect(resp.results.length).to be >= 2 + end + end describe 'SearchQueryAndMessageFiltersError' do + it 'verifies error when using both query and message_filter_conditions' do + + # rubocop:disable Layout/EmptyLinesAroundArguments expect do + search_messages( - filter_conditions: { 'members' => { '$in' => [@user1] } }, + filter_conditions: { 'members' => { '$in' => [@user_1] } }, query: 'test', - message_filter_conditions: { 'text' => { '$q' => 'test' } } + message_filter_conditions: { 'text' => { '$q' => 'test' } }, ) + end.to raise_error(GetStreamRuby::APIError) + # rubocop:enable Layout/EmptyLinesAroundArguments + end + end describe 'SearchOffsetAndSortError' do + it 'verifies error when using offset with sort' do + # The API may or may not reject offset+sort. Verify either an error or a valid response. - begin - resp = search_messages( - filter_conditions: { 'members' => { '$in' => [@user1] } }, - query: 'test', - offset: 1, - sort: [{ 'field' => 'created_at', 'direction' => -1 }] - ) - # If no error, the API accepts the combination — verify a valid response - expect(resp).not_to be_nil - rescue GetStreamRuby::APIError - # Expected error — test passes - end + + resp = search_messages( + filter_conditions: { 'members' => { '$in' => [@user_1] } }, + query: 'test', + offset: 1, + sort: [{ 'field' => 'created_at', 'direction' => -1 }], + ) + # If no error, the API accepts the combination — verify a valid response + expect(resp).not_to be_nil + rescue GetStreamRuby::APIError + # Expected error — test passes + end + end describe 'SearchOffsetAndNextError' do + it 'verifies error when using offset with next' do + + # rubocop:disable Layout/EmptyLinesAroundArguments expect do + search_messages( - filter_conditions: { 'members' => { '$in' => [@user1] } }, + filter_conditions: { 'members' => { '$in' => [@user_1] } }, query: 'test', offset: 1, - next: SecureRandom.hex(5) + next: SecureRandom.hex(5), ) + end.to raise_error(GetStreamRuby::APIError) + # rubocop:enable Layout/EmptyLinesAroundArguments + end + end describe 'ChannelRoleInMember' do + it 'creates channel with roles, sends messages, verifies member.channel_role in response' do + role_user_ids, _resp = create_test_users(2) member_user_id = role_user_ids[0] mod_user_id = role_user_ids[1] @@ -579,10 +699,10 @@ def undelete_message(message_id, body) created_by_id: member_user_id, members: [ { user_id: member_user_id, channel_role: 'channel_member' }, - { user_id: mod_user_id, channel_role: 'channel_moderator' } - ] - } - } + { user_id: mod_user_id, channel_role: 'channel_moderator' }, + ], + }, + }, ) @created_channel_cids << "messaging:#{channel_id}" @@ -599,6 +719,9 @@ def undelete_message(message_id, body) expect(resp_mod.message).not_to be_nil mod_data = resp_mod.message.to_h['member'] || {} expect(mod_data['channel_role']).to eq('channel_moderator') + end + end + end diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb index be6adef..2b4e344 100644 --- a/spec/integration/chat_misc_integration_spec.rb +++ b/spec/integration/chat_misc_integration_spec.rb @@ -6,59 +6,76 @@ require_relative 'chat_test_helpers' RSpec.describe 'Chat Misc Integration', type: :integration do + include ChatTestHelpers before(:all) do + init_chat_client @shared_user_ids, _resp = create_test_users(4) - @user1 = @shared_user_ids[0] - @user2 = @shared_user_ids[1] - @user3 = @shared_user_ids[2] - @user4 = @shared_user_ids[3] + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @user_3 = @shared_user_ids[2] + @user_4 = @shared_user_ids[3] @created_blocklist_names = [] @created_command_names = [] @created_channel_type_names = [] @created_role_names = [] + end after(:all) do + # Clean up blocklists @created_blocklist_names&.each do |name| + @client.common.delete_block_list(name) rescue StandardError => e puts "Warning: Failed to delete blocklist #{name}: #{e.message}" + end # Clean up commands @created_command_names&.each do |name| + @client.make_request(:delete, "/api/v2/chat/commands/#{name}") rescue StandardError => e puts "Warning: Failed to delete command #{name}: #{e.message}" + end # Clean up channel types (with retry due to eventual consistency) @created_channel_type_names&.each do |name| + 3.times do |i| + @client.make_request(:delete, "/api/v2/chat/channeltypes/#{name}") break rescue StandardError => e puts "Warning: Failed to delete channel type #{name} (attempt #{i + 1}): #{e.message}" sleep(1) + end + end # Clean up roles @created_role_names&.each do |name| + 3.times do |i| + @client.common.delete_role(name) break rescue StandardError => e puts "Warning: Failed to delete role #{name} (attempt #{i + 1}): #{e.message}" sleep(1) + end + end cleanup_chat_resources + end # --------------------------------------------------------------------------- @@ -66,7 +83,9 @@ # --------------------------------------------------------------------------- describe 'CreateListDeleteDevice' do + it 'creates a firebase device, lists it, deletes it, and verifies gone' do + device_id = "integration-test-device-#{random_string(12)}" # Create device @@ -74,28 +93,44 @@ GetStream::Generated::Models::CreateDeviceRequest.new( id: device_id, push_provider: 'firebase', - user_id: @user1 - ) + user_id: @user_1, + ), ) # List devices - list_resp = @client.common.list_devices(@user1) + list_resp = @client.common.list_devices(@user_1) devices = list_resp.devices || [] - found = devices.any? { |d| h = d.is_a?(Hash) ? d : d.to_h; h['id'] == device_id } - expect(found).to be(true), "Created device should appear in list" + found = devices.any? do |d| + + h = d.is_a?(Hash) ? d : d.to_h + + h['id'] == device_id + + end + expect(found).to be(true), 'Created device should appear in list' # Delete device - @client.common.delete_device(device_id, @user1) + @client.common.delete_device(device_id, @user_1) # Verify deleted - list_resp2 = @client.common.list_devices(@user1) - devices2 = list_resp2.devices || [] - still_found = devices2.any? { |d| h = d.is_a?(Hash) ? d : d.to_h; h['id'] == device_id } - expect(still_found).to be(false), "Device should be deleted" + list_resp_2 = @client.common.list_devices(@user_1) + devices_2 = list_resp_2.devices || [] + still_found = devices_2.any? do |d| + + h = d.is_a?(Hash) ? d : d.to_h + + h['id'] == device_id + + end + expect(still_found).to be(false), 'Device should be deleted' rescue GetStreamRuby::APIError => e - skip('Push providers not configured for this app') if e.message.include?('push provider') || e.message.include?('no push') + if e.message.include?('push provider') || e.message.include?('no push') + skip('Push providers not configured for this app') + end raise + end + end # --------------------------------------------------------------------------- @@ -103,15 +138,17 @@ # --------------------------------------------------------------------------- describe 'CreateListDeleteBlocklist' do + it 'creates a custom blocklist, lists it, verifies found, and deletes it' do + blocklist_name = "test-blocklist-#{random_string(8)}" # Create blocklist @client.common.create_block_list( GetStream::Generated::Models::CreateBlockListRequest.new( name: blocklist_name, - words: %w[badword1 badword2 badword3] - ) + words: %w[badword1 badword2 badword3], + ), ) @created_blocklist_names << blocklist_name @@ -126,36 +163,40 @@ @client.common.update_block_list( blocklist_name, GetStream::Generated::Models::UpdateBlockListRequest.new( - words: %w[badword1 badword2 badword3 badword4] - ) + words: %w[badword1 badword2 badword3 badword4], + ), ) # Verify update - get_resp2 = @client.common.get_block_list(blocklist_name) - bl_h2 = get_resp2.blocklist.to_h - expect(bl_h2['words'].length).to eq(4) + get_resp_2 = @client.common.get_block_list(blocklist_name) + bl_h_2 = get_resp_2.blocklist.to_h + expect(bl_h_2['words'].length).to eq(4) # List blocklists and verify found list_resp = @client.common.list_block_lists blocklists = list_resp.blocklists || [] found = blocklists.any? do |bl| + h = bl.is_a?(Hash) ? bl : bl.to_h h['name'] == blocklist_name + end - expect(found).to be(true), "Created blocklist should appear in list" + expect(found).to be(true), 'Created blocklist should appear in list' # Delete a separate blocklist to test deletion del_name = "test-del-bl-#{random_string(8)}" @client.common.create_block_list( GetStream::Generated::Models::CreateBlockListRequest.new( name: del_name, - words: %w[word1] - ) + words: %w[word1], + ), ) @created_blocklist_names << del_name @client.common.delete_block_list(del_name) @created_blocklist_names.delete(del_name) + end + end # --------------------------------------------------------------------------- @@ -163,14 +204,16 @@ # --------------------------------------------------------------------------- describe 'CreateListDeleteCommand' do + it 'creates a custom command, lists it, verifies found, and deletes it' do + cmd_name = "testcmd#{random_string(6)}" # Create command resp = @client.make_request(:post, '/api/v2/chat/commands', body: { - name: cmd_name, - description: 'A test command' - }) + name: cmd_name, + description: 'A test command', + }) expect(resp).not_to be_nil @created_command_names << cmd_name @@ -181,31 +224,35 @@ # Update command @client.make_request(:put, "/api/v2/chat/commands/#{cmd_name}", body: { - description: 'Updated test command' - }) + description: 'Updated test command', + }) # Verify update - get_resp2 = @client.make_request(:get, "/api/v2/chat/commands/#{cmd_name}") - expect(get_resp2.description).to eq('Updated test command') + get_resp_2 = @client.make_request(:get, "/api/v2/chat/commands/#{cmd_name}") + expect(get_resp_2.description).to eq('Updated test command') # List commands list_resp = @client.make_request(:get, '/api/v2/chat/commands') commands = list_resp.commands || [] found = commands.any? do |c| + h = c.is_a?(Hash) ? c : c.to_h h['name'] == cmd_name + end - expect(found).to be(true), "Created command should appear in list" + expect(found).to be(true), 'Created command should appear in list' # Delete a separate command del_name = "testdelcmd#{random_string(6)}" @client.make_request(:post, '/api/v2/chat/commands', body: { - name: del_name, - description: 'Command to delete' - }) + name: del_name, + description: 'Command to delete', + }) del_resp = @client.make_request(:delete, "/api/v2/chat/commands/#{del_name}") expect(del_resp).not_to be_nil + end + end # --------------------------------------------------------------------------- @@ -213,16 +260,18 @@ # --------------------------------------------------------------------------- describe 'CreateUpdateDeleteChannelType' do + it 'creates a channel type, updates settings, verifies, and deletes' do + type_name = "testtype#{random_string(6)}" # Create channel type create_resp = @client.make_request(:post, '/api/v2/chat/channeltypes', body: { - name: type_name, - automod: 'disabled', - automod_behavior: 'flag', - max_message_length: 5000 - }) + name: type_name, + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 5000, + }) expect(create_resp.name).to eq(type_name) @created_channel_type_names << type_name @@ -235,49 +284,55 @@ # Update channel type update_resp = @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { - automod: 'disabled', - automod_behavior: 'flag', - max_message_length: 10_000, - typing_events: false - }) + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 10_000, + typing_events: false, + }) expect(update_resp.max_message_length).to eq(10_000) # Delete a separate channel type del_name = "testdeltype#{random_string(6)}" @client.make_request(:post, '/api/v2/chat/channeltypes', body: { - name: del_name, - automod: 'disabled', - automod_behavior: 'flag', - max_message_length: 5000 - }) + name: del_name, + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 5000, + }) @created_channel_type_names << del_name sleep(2) delete_err = nil - 5.times do |i| - begin - @client.make_request(:delete, "/api/v2/chat/channeltypes/#{del_name}") - @created_channel_type_names.delete(del_name) - delete_err = nil - break - rescue StandardError => e - delete_err = e - sleep(2) - end + 5.times do |_i| + + @client.make_request(:delete, "/api/v2/chat/channeltypes/#{del_name}") + @created_channel_type_names.delete(del_name) + delete_err = nil + break + rescue StandardError => e + delete_err = e + sleep(2) + end expect(delete_err).to be_nil, "Channel type deletion should succeed: #{delete_err&.message}" + end + end describe 'ListChannelTypes' do + it 'lists all channel types and verifies default types present' do + resp = @client.make_request(:get, '/api/v2/chat/channeltypes') expect(resp.channel_types).not_to be_nil types_h = resp.channel_types.to_h expect(types_h.key?('messaging')).to be(true), "Default 'messaging' type should be present" + end + end # --------------------------------------------------------------------------- @@ -285,20 +340,26 @@ # --------------------------------------------------------------------------- describe 'ListPermissions' do + it 'lists all permissions and verifies non-empty' do + resp = @client.common.list_permissions expect(resp.permissions).not_to be_nil expect(resp.permissions.length).to be > 0 + end + end describe 'CreatePermission' do + it 'creates a custom role, lists it, and verifies custom flag' do + role_name = "testrole#{random_string(6)}" # Create role @client.common.create_role( - GetStream::Generated::Models::CreateRoleRequest.new(name: role_name) + GetStream::Generated::Models::CreateRoleRequest.new(name: role_name), ) @created_role_names << role_name @@ -306,22 +367,30 @@ list_resp = @client.common.list_roles roles = list_resp.roles || [] found = roles.any? do |r| + h = r.is_a?(Hash) ? r : r.to_h h['name'] == role_name && h['custom'] == true + end - expect(found).to be(true), "Created role should appear in list as custom" + expect(found).to be(true), 'Created role should appear in list as custom' + end + end describe 'GetPermission' do + it 'gets a specific permission by ID' do + resp = @client.common.get_permission('create-channel') expect(resp.permission).not_to be_nil perm_h = resp.permission.to_h expect(perm_h['id']).to eq('create-channel') expect(perm_h['action']).not_to be_nil expect(perm_h['action']).not_to be_empty + end + end # --------------------------------------------------------------------------- @@ -329,27 +398,30 @@ # --------------------------------------------------------------------------- describe 'QueryBannedUsers' do + it 'bans a user in channel, queries banned users, and verifies' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) cid = "messaging:#{channel_id}" # Ban user in channel @client.moderation.ban( GetStream::Generated::Models::BanRequest.new( - target_user_id: @user2, - banned_by_id: @user1, + target_user_id: @user_2, + banned_by_id: @user_1, channel_cid: cid, reason: 'test ban reason', - timeout: 60 - ) + timeout: 60, + ), ) # Query banned users - resp = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { - 'payload' => JSON.generate({ - filter_conditions: { 'channel_cid' => { '$eq' => cid } } - }) - }) + payload = JSON.generate({ filter_conditions: { 'channel_cid' => { '$eq' => cid } } }) + resp = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) bans = resp.bans || [] expect(bans.length).to be >= 1 @@ -359,19 +431,22 @@ # Unban @client.moderation.unban( GetStream::Generated::Models::UnbanRequest.new, - @user2, - cid + @user_2, + cid, ) # Verify ban is gone - resp2 = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { - 'payload' => JSON.generate({ - filter_conditions: { 'channel_cid' => { '$eq' => cid } } - }) - }) - bans2 = resp2.bans || [] - expect(bans2.length).to eq(0), "Bans should be empty after unban" + payload = JSON.generate({ filter_conditions: { 'channel_cid' => { '$eq' => cid } } }) + resp_2 = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans_2 = resp_2.bans || [] + expect(bans_2.length).to eq(0), 'Bans should be empty after unban' + end + end # --------------------------------------------------------------------------- @@ -379,13 +454,15 @@ # --------------------------------------------------------------------------- describe 'MuteUnmuteUser' do + it 'mutes user, verifies via query, and unmutes' do + # Mute user mute_resp = @client.moderation.mute( GetStream::Generated::Models::MuteRequest.new( - target_ids: [@user3], - user_id: @user1 - ) + target_ids: [@user_3], + user_id: @user_1, + ), ) expect(mute_resp.mutes).not_to be_nil expect(mute_resp.mutes.length).to be >= 1 @@ -395,8 +472,8 @@ # Verify via QueryUsers that user has mutes q_resp = @client.common.query_users(JSON.generate({ - filter_conditions: { 'id' => { '$eq' => @user1 } } - })) + filter_conditions: { 'id' => { '$eq' => @user_1 } }, + })) expect(q_resp.users).not_to be_nil expect(q_resp.users.length).to be >= 1 user_h = q_resp.users[0].is_a?(Hash) ? q_resp.users[0] : q_resp.users[0].to_h @@ -406,11 +483,13 @@ # Unmute @client.moderation.unmute( GetStream::Generated::Models::UnmuteRequest.new( - target_ids: [@user3], - user_id: @user1 - ) + target_ids: [@user_3], + user_id: @user_1, + ), ) + end + end # --------------------------------------------------------------------------- @@ -418,14 +497,18 @@ # --------------------------------------------------------------------------- describe 'GetAppSettings' do + it 'gets app settings and verifies response' do + resp = @client.common.get_app expect(resp).not_to be_nil expect(resp.app).not_to be_nil app_h = resp.app.to_h expect(app_h['name']).not_to be_nil expect(app_h['name']).not_to be_empty + end + end # --------------------------------------------------------------------------- @@ -433,23 +516,27 @@ # --------------------------------------------------------------------------- describe 'ExportChannels' do + it 'exports channel messages and polls task until completed' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - send_test_message('messaging', channel_id, @user1, "Message for export test #{SecureRandom.hex(4)}") + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + send_test_message('messaging', channel_id, @user_1, "Message for export test #{SecureRandom.hex(4)}") cid = "messaging:#{channel_id}" # Export channels export_resp = @client.make_request(:post, '/api/v2/chat/export_channels', body: { - channels: [{ cid: cid }] - }) + channels: [{ cid: cid }], + }) expect(export_resp.task_id).not_to be_nil expect(export_resp.task_id).not_to be_empty # Wait for task task_result = wait_for_task(export_resp.task_id) expect(task_result.status).to eq('completed') + end + end # --------------------------------------------------------------------------- @@ -457,54 +544,60 @@ # --------------------------------------------------------------------------- describe 'Threads' do + it 'creates parent + replies, queries threads, and verifies' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) channel_cid = "messaging:#{channel_id}" # Create thread: parent message + replies - parent_id = send_test_message('messaging', channel_id, @user1, 'Thread parent message') + parent_id = send_test_message('messaging', channel_id, @user_1, 'Thread parent message') send_message('messaging', channel_id, { - message: { - text: 'First reply in thread', - user_id: @user2, - parent_id: parent_id - } - }) + message: { + text: 'First reply in thread', + user_id: @user_2, + parent_id: parent_id, + }, + }) send_message('messaging', channel_id, { - message: { - text: 'Second reply in thread', - user_id: @user1, - parent_id: parent_id - } - }) + message: { + text: 'Second reply in thread', + user_id: @user_1, + parent_id: parent_id, + }, + }) # Query threads resp = @client.make_request(:post, '/api/v2/chat/threads', body: { - user_id: @user1, - filter: { - 'channel_cid' => { '$eq' => channel_cid } - } - }) + user_id: @user_1, + filter: { + 'channel_cid' => { '$eq' => channel_cid }, + }, + }) expect(resp.threads).not_to be_nil expect(resp.threads.length).to be >= 1 found = resp.threads.any? do |t| + h = t.is_a?(Hash) ? t : t.to_h h['parent_message_id'] == parent_id + end - expect(found).to be(true), "Thread should appear in query results" + expect(found).to be(true), 'Thread should appear in query results' # Get thread get_resp = @client.make_request(:get, "/api/v2/chat/threads/#{parent_id}", query_params: { - 'reply_limit' => '10' - }) + 'reply_limit' => '10', + }) thread_h = get_resp.thread.is_a?(Hash) ? get_resp.thread : get_resp.thread.to_h expect(thread_h['parent_message_id']).to eq(parent_id) latest_replies = thread_h['latest_replies'] || [] expect(latest_replies.length).to be >= 2 + end + end # --------------------------------------------------------------------------- @@ -512,32 +605,40 @@ # --------------------------------------------------------------------------- describe 'GetUnreadCounts' do + it 'sends message and gets unread counts for user' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) - send_test_message('messaging', channel_id, @user1, "Unread test #{SecureRandom.hex(4)}") + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + send_test_message('messaging', channel_id, @user_1, "Unread test #{SecureRandom.hex(4)}") resp = @client.make_request(:get, '/api/v2/chat/unread', query_params: { - 'user_id' => @user2 - }) + 'user_id' => @user_2, + }) expect(resp).not_to be_nil expect(resp.total_unread_count).to be >= 0 + end + end describe 'GetUnreadCountsBatch' do + it 'gets unread counts for multiple users' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) - send_test_message('messaging', channel_id, @user1, "Batch unread test #{SecureRandom.hex(4)}") + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + send_test_message('messaging', channel_id, @user_1, "Batch unread test #{SecureRandom.hex(4)}") resp = @client.make_request(:post, '/api/v2/chat/unread_batch', body: { - user_ids: [@user1, @user2] - }) + user_ids: [@user_1, @user_2], + }) expect(resp).not_to be_nil expect(resp.counts_by_user).not_to be_nil counts_h = resp.counts_by_user.to_h - expect(counts_h.key?(@user1)).to be(true) - expect(counts_h.key?(@user2)).to be(true) + expect(counts_h.key?(@user_1)).to be(true) + expect(counts_h.key?(@user_2)).to be(true) + end + end # --------------------------------------------------------------------------- @@ -545,44 +646,48 @@ # --------------------------------------------------------------------------- describe 'Reminders' do + it 'creates a reminder, lists it, updates it, and deletes it' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, "Reminder test #{SecureRandom.hex(4)}") - remind_at = (Time.now + 24 * 3600).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, "Reminder test #{SecureRandom.hex(4)}") + + remind_at = (Time.now + (24 * 3600)).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') # Create reminder create_resp = @client.make_request(:post, "/api/v2/chat/messages/#{msg_id}/reminders", body: { - user_id: @user1, - remind_at: remind_at - }) + user_id: @user_1, + remind_at: remind_at, + }) expect(create_resp).not_to be_nil # Query reminders query_resp = @client.make_request(:post, '/api/v2/chat/reminders/query', body: { - user_id: @user1, - filter: { 'message_id' => msg_id }, - sort: [] - }) + user_id: @user_1, + filter: { 'message_id' => msg_id }, + sort: [], + }) reminders = query_resp.reminders || [] expect(reminders.length).to be >= 1 # Update reminder - new_remind_at = (Time.now + 48 * 3600).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') + new_remind_at = (Time.now + (48 * 3600)).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') update_resp = @client.make_request(:patch, "/api/v2/chat/messages/#{msg_id}/reminders", body: { - user_id: @user1, - remind_at: new_remind_at - }) + user_id: @user_1, + remind_at: new_remind_at, + }) expect(update_resp).not_to be_nil # Delete reminder @client.make_request(:delete, "/api/v2/chat/messages/#{msg_id}/reminders", query_params: { - 'user_id' => @user1 - }) + 'user_id' => @user_1, + }) rescue GetStreamRuby::APIError => e skip('Reminders not enabled for this app') if e.message.include?('not enabled') || e.message.include?('reminder') raise + end + end # --------------------------------------------------------------------------- @@ -590,15 +695,19 @@ # --------------------------------------------------------------------------- describe 'SendUserCustomEvent' do + it 'sends a custom event to a user' do - resp = @client.make_request(:post, "/api/v2/chat/users/#{@user1}/event", body: { - event: { - type: 'friendship_request', - message: "Let's be friends!" - } - }) + + resp = @client.make_request(:post, "/api/v2/chat/users/#{@user_1}/event", body: { + event: { + type: 'friendship_request', + message: "Let's be friends!", + }, + }) expect(resp).not_to be_nil + end + end # --------------------------------------------------------------------------- @@ -606,13 +715,22 @@ # --------------------------------------------------------------------------- describe 'QueryTeamUsageStats' do + it 'queries team usage stats' do + resp = @client.make_request(:post, '/api/v2/chat/stats/team_usage', body: {}) expect(resp).not_to be_nil rescue GetStreamRuby::APIError => e - skip('QueryTeamUsageStats not available on this app') if e.message.include?('Token signature') || e.message.include?('not available') || e.message.include?('not found') || e.message.include?('Not Found') + if e.message.include?('Token signature') || + e.message.include?('not available') || + e.message.include?('not found') || + e.message.include?('Not Found') + skip('QueryTeamUsageStats not available on this app') + end raise + end + end # --------------------------------------------------------------------------- @@ -620,23 +738,33 @@ # --------------------------------------------------------------------------- describe 'ChannelBatchUpdate' do + it 'batch updates multiple channels at once' do - _type1, ch_id1, _resp1 = create_test_channel(@user1) - _type2, ch_id2, _resp2 = create_test_channel(@user1) + + _type_1, ch_id_1, _resp_1 = create_test_channel(@user_1) + _type_2, ch_id_2, _resp_2 = create_test_channel(@user_1) # Batch update: set a custom field on both channels - cids = ["messaging:#{ch_id1}", "messaging:#{ch_id2}"] + cids = ["messaging:#{ch_id_1}", "messaging:#{ch_id_2}"] resp = @client.make_request(:post, '/api/v2/chat/channels/batch_update', body: { - set: { 'color' => 'blue' }, - filter: { - 'cid' => { '$in' => cids } - } - }) + set: { 'color' => 'blue' }, + filter: { + 'cid' => { '$in' => cids }, + }, + }) expect(resp).not_to be_nil rescue GetStreamRuby::APIError => e - skip('Channel batch update not available') if e.message.include?('not available') || e.message.include?('Not Found') || e.message.include?('unknown') || e.message.include?('not found') + if e.message.include?('not available') || + e.message.include?('Not Found') || + e.message.include?('unknown') || + e.message.include?('not found') + skip('Channel batch update not available') + end raise + end + end + end diff --git a/spec/integration/chat_moderation_integration_spec.rb b/spec/integration/chat_moderation_integration_spec.rb index e83c9c5..dfc627f 100644 --- a/spec/integration/chat_moderation_integration_spec.rb +++ b/spec/integration/chat_moderation_integration_spec.rb @@ -6,19 +6,24 @@ require_relative 'chat_test_helpers' RSpec.describe 'Chat Moderation Integration', type: :integration do + include ChatTestHelpers before(:all) do + init_chat_client @shared_user_ids, _resp = create_test_users(4) - @user1 = @shared_user_ids[0] - @user2 = @shared_user_ids[1] - @user3 = @shared_user_ids[2] - @user4 = @shared_user_ids[3] + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @user_3 = @shared_user_ids[2] + @user_4 = @shared_user_ids[3] + end after(:all) do + cleanup_chat_resources + end # --------------------------------------------------------------------------- @@ -26,96 +31,110 @@ # --------------------------------------------------------------------------- describe 'BanUnbanUser' do + it 'bans a user from a channel, verifies, and unbans' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) cid = "messaging:#{channel_id}" # Ban user in channel @client.moderation.ban( GetStream::Generated::Models::BanRequest.new( - target_user_id: @user2, - banned_by_id: @user1, + target_user_id: @user_2, + banned_by_id: @user_1, channel_cid: cid, reason: 'moderation test ban', - timeout: 60 - ) + timeout: 60, + ), ) # Verify via query banned users - resp = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { - 'payload' => JSON.generate({ - filter_conditions: { 'channel_cid' => { '$eq' => cid } } - }) - }) + payload = JSON.generate({ filter_conditions: { 'channel_cid' => { '$eq' => cid } } }) + resp = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) bans = resp.bans || [] expect(bans.length).to be >= 1 banned_user_ids = bans.map do |b| + h = b.is_a?(Hash) ? b : b.to_h target = h['user'] || {} target = target.is_a?(Hash) ? target : target.to_h target['id'] + end - expect(banned_user_ids).to include(@user2) + expect(banned_user_ids).to include(@user_2) # Unban user @client.moderation.unban( GetStream::Generated::Models::UnbanRequest.new, - @user2, - cid + @user_2, + cid, ) # Verify ban is removed - resp2 = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { - 'payload' => JSON.generate({ - filter_conditions: { 'channel_cid' => { '$eq' => cid } } - }) - }) - bans2 = resp2.bans || [] - banned_ids_after = bans2.map do |b| + payload = JSON.generate({ filter_conditions: { 'channel_cid' => { '$eq' => cid } } }) + resp_2 = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans_2 = resp_2.bans || [] + banned_ids_after = bans_2.map do |b| + h = b.is_a?(Hash) ? b : b.to_h target = h['user'] || {} target = target.is_a?(Hash) ? target : target.to_h target['id'] + end - expect(banned_ids_after).not_to include(@user2) + expect(banned_ids_after).not_to include(@user_2) + end it 'bans a user app-wide, verifies, and unbans' do + # Ban user app-wide (no channel_cid) @client.moderation.ban( GetStream::Generated::Models::BanRequest.new( - target_user_id: @user3, - banned_by_id: @user1, + target_user_id: @user_3, + banned_by_id: @user_1, reason: 'app-wide moderation test ban', - timeout: 60 - ) + timeout: 60, + ), ) # Verify via query banned users (app-level) - resp = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { - 'payload' => JSON.generate({ - filter_conditions: { 'user_id' => { '$eq' => @user3 } } - }) - }) + payload = JSON.generate({ filter_conditions: { 'user_id' => { '$eq' => @user_3 } } }) + resp = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) bans = resp.bans || [] expect(bans.length).to be >= 1 # Unban user app-wide @client.moderation.unban( GetStream::Generated::Models::UnbanRequest.new, - @user3 + @user_3, ) # Verify ban is removed - resp2 = @client.make_request(:get, '/api/v2/chat/query_banned_users', query_params: { - 'payload' => JSON.generate({ - filter_conditions: { 'user_id' => { '$eq' => @user3 } } - }) - }) - bans2 = resp2.bans || [] - expect(bans2.length).to eq(0), "App-wide ban should be removed after unban" + payload = JSON.generate({ filter_conditions: { 'user_id' => { '$eq' => @user_3 } } }) + resp_2 = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans_2 = resp_2.bans || [] + expect(bans_2.length).to eq(0), 'App-wide ban should be removed after unban' + end + end # --------------------------------------------------------------------------- @@ -123,13 +142,15 @@ # --------------------------------------------------------------------------- describe 'MuteUnmuteUser' do + it 'mutes a user, verifies via query, and unmutes' do + # Mute user mute_resp = @client.moderation.mute( GetStream::Generated::Models::MuteRequest.new( - target_ids: [@user4], - user_id: @user1 - ) + target_ids: [@user_4], + user_id: @user_1, + ), ) expect(mute_resp.mutes).not_to be_nil expect(mute_resp.mutes.length).to be >= 1 @@ -137,12 +158,12 @@ mute_h = mute_resp.mutes[0].is_a?(Hash) ? mute_resp.mutes[0] : mute_resp.mutes[0].to_h target = mute_h['target'] || {} target = target.is_a?(Hash) ? target : target.to_h - expect(target['id']).to eq(@user4) + expect(target['id']).to eq(@user_4) # Verify via QueryUsers that muter has mutes q_resp = @client.common.query_users(JSON.generate({ - filter_conditions: { 'id' => { '$eq' => @user1 } } - })) + filter_conditions: { 'id' => { '$eq' => @user_1 } }, + })) expect(q_resp.users).not_to be_nil expect(q_resp.users.length).to be >= 1 user_h = q_resp.users[0].is_a?(Hash) ? q_resp.users[0] : q_resp.users[0].to_h @@ -150,35 +171,41 @@ expect(user_h['mutes'].length).to be >= 1 muted_ids = user_h['mutes'].map do |m| + t = m.is_a?(Hash) ? m : m.to_h tgt = t['target'] || {} tgt = tgt.is_a?(Hash) ? tgt : tgt.to_h tgt['id'] + end - expect(muted_ids).to include(@user4) + expect(muted_ids).to include(@user_4) # Unmute user @client.moderation.unmute( GetStream::Generated::Models::UnmuteRequest.new( - target_ids: [@user4], - user_id: @user1 - ) + target_ids: [@user_4], + user_id: @user_1, + ), ) # Verify mute is removed - q_resp2 = @client.common.query_users(JSON.generate({ - filter_conditions: { 'id' => { '$eq' => @user1 } } - })) - user_h2 = q_resp2.users[0].is_a?(Hash) ? q_resp2.users[0] : q_resp2.users[0].to_h - mutes_after = user_h2['mutes'] || [] + q_resp_2 = @client.common.query_users(JSON.generate({ + filter_conditions: { 'id' => { '$eq' => @user_1 } }, + })) + user_h_2 = q_resp_2.users[0].is_a?(Hash) ? q_resp_2.users[0] : q_resp_2.users[0].to_h + mutes_after = user_h_2['mutes'] || [] muted_ids_after = mutes_after.map do |m| + t = m.is_a?(Hash) ? m : m.to_h tgt = t['target'] || {} tgt = tgt.is_a?(Hash) ? tgt : tgt.to_h tgt['id'] + end - expect(muted_ids_after).not_to include(@user4) + expect(muted_ids_after).not_to include(@user_4) + end + end # --------------------------------------------------------------------------- @@ -186,35 +213,42 @@ # --------------------------------------------------------------------------- describe 'FlagMessageAndUser' do + it 'flags a message and verifies response' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) - msg_id = send_test_message('messaging', channel_id, @user1, "Flaggable message #{SecureRandom.hex(4)}") + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + msg_id = send_test_message('messaging', channel_id, @user_1, "Flaggable message #{SecureRandom.hex(4)}") # Flag message flag_resp = @client.moderation.flag( GetStream::Generated::Models::FlagRequest.new( entity_type: 'stream:chat:v1:message', entity_id: msg_id, - entity_creator_id: @user1, + entity_creator_id: @user_1, reason: 'inappropriate content', - user_id: @user2 - ) + user_id: @user_2, + ), ) expect(flag_resp).not_to be_nil + end it 'flags a user and verifies response' do + # Flag user flag_resp = @client.moderation.flag( GetStream::Generated::Models::FlagRequest.new( entity_type: 'stream:user', - entity_id: @user3, - entity_creator_id: @user3, + entity_id: @user_3, + entity_creator_id: @user_3, reason: 'spam behavior', - user_id: @user1 - ) + user_id: @user_1, + ), ) expect(flag_resp).not_to be_nil + end + end + end diff --git a/spec/integration/chat_polls_integration_spec.rb b/spec/integration/chat_polls_integration_spec.rb index 6916f48..c41b388 100644 --- a/spec/integration/chat_polls_integration_spec.rb +++ b/spec/integration/chat_polls_integration_spec.rb @@ -6,25 +6,32 @@ require_relative 'chat_test_helpers' RSpec.describe 'Chat Polls Integration', type: :integration do + include ChatTestHelpers before(:all) do + init_chat_client @shared_user_ids, _resp = create_test_users(2) - @user1 = @shared_user_ids[0] - @user2 = @shared_user_ids[1] + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] @created_poll_ids = [] + end after(:all) do + # Delete polls before channels/users (polls reference users) @created_poll_ids&.each do |poll_id| - @client.common.delete_poll(poll_id, @user1) + + @client.common.delete_poll(poll_id, @user_1) rescue StandardError => e puts "Warning: Failed to delete poll #{poll_id}: #{e.message}" + end cleanup_chat_resources + end # --------------------------------------------------------------------------- @@ -33,7 +40,9 @@ def create_poll(name, user_id, options: [], enforce_unique_vote: nil, description: nil) poll_options = options.map do |text| + GetStream::Generated::Models::PollOptionInput.new(text: text) + end req = GetStream::Generated::Models::CreatePollRequest.new( @@ -41,7 +50,7 @@ def create_poll(name, user_id, options: [], enforce_unique_vote: nil, descriptio user_id: user_id, options: poll_options, enforce_unique_vote: enforce_unique_vote, - description: description + description: description, ) resp = @client.common.create_poll(req) @@ -66,12 +75,12 @@ def delete_poll(poll_id, user_id) def cast_poll_vote(message_id, poll_id, user_id, option_id) body = { user_id: user_id, - vote: { option_id: option_id } + vote: { option_id: option_id }, } @client.make_request( :post, "/api/v2/chat/messages/#{message_id}/polls/#{poll_id}/vote", - body: body + body: body, ) end @@ -80,16 +89,18 @@ def cast_poll_vote(message_id, poll_id, user_id, option_id) # --------------------------------------------------------------------------- describe 'CreateAndQueryPoll' do + it 'creates a poll with options, gets it, and queries it' do + poll_name = "Favorite color? #{SecureRandom.hex(4)}" # Create poll with options create_resp = create_poll( poll_name, - @user1, + @user_1, options: %w[Red Blue Green], enforce_unique_vote: true, - description: 'Pick your favorite color' + description: 'Pick your favorite color', ) expect(create_resp.poll).not_to be_nil poll_id = create_resp.poll.id @@ -107,30 +118,36 @@ def cast_poll_vote(message_id, poll_id, user_id, option_id) expect(get_resp.poll.name).to eq(poll_name) # Query polls with filter - query_resp = query_polls({ 'id' => poll_id }, @user1) + query_resp = query_polls({ 'id' => poll_id }, @user_1) expect(query_resp.polls).not_to be_nil expect(query_resp.polls.length).to be >= 1 found = query_resp.polls.any? do |p| + h = p.is_a?(Hash) ? p : p.to_h h['id'] == poll_id + end expect(found).to be true rescue StandardError => e skip('Polls not enabled for this app') if e.message.include?('Polls') || e.message.include?('polls') raise + end + end describe 'CastPollVote' do + it 'creates a poll, attaches to message, casts vote, and verifies' do + # Create poll poll_name = "Vote test #{SecureRandom.hex(4)}" create_resp = create_poll( poll_name, - @user1, + @user_1, options: %w[Yes No], - enforce_unique_vote: true + enforce_unique_vote: true, ) poll_id = create_resp.poll.id poll_h = create_resp.poll.to_h @@ -138,22 +155,22 @@ def cast_poll_vote(message_id, poll_id, user_id, option_id) expect(option_id).not_to be_nil # Create channel with both users as members - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) # Send message with poll attached body = { message: { text: 'Please vote!', - user_id: @user1, - poll_id: poll_id - } + user_id: @user_1, + poll_id: poll_id, + }, } msg_resp = send_message('messaging', channel_id, body) msg_id = msg_resp.message.id expect(msg_id).not_to be_nil # Cast a vote as user2 - vote_resp = cast_poll_vote(msg_id, poll_id, @user2, option_id) + vote_resp = cast_poll_vote(msg_id, poll_id, @user_2, option_id) expect(vote_resp.vote).not_to be_nil vote_h = vote_resp.vote.to_h expect(vote_h['option_id']).to eq(option_id) @@ -164,6 +181,9 @@ def cast_poll_vote(message_id, poll_id, user_id, option_id) rescue StandardError => e skip('Polls not enabled for this app') if e.message.include?('Polls') || e.message.include?('polls') raise + end + end + end diff --git a/spec/integration/chat_reaction_integration_spec.rb b/spec/integration/chat_reaction_integration_spec.rb index 7470d0b..d24db9a 100644 --- a/spec/integration/chat_reaction_integration_spec.rb +++ b/spec/integration/chat_reaction_integration_spec.rb @@ -6,17 +6,22 @@ require_relative 'chat_test_helpers' RSpec.describe 'Chat Reaction Integration', type: :integration do + include ChatTestHelpers before(:all) do + init_chat_client @shared_user_ids, _resp = create_test_users(2) - @user1 = @shared_user_ids[0] - @user2 = @shared_user_ids[1] + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + end after(:all) do + cleanup_chat_resources + end # --------------------------------------------------------------------------- @@ -25,7 +30,7 @@ def send_reaction(message_id, reaction_type, user_id, enforce_unique: false) body = { - reaction: { type: reaction_type, user_id: user_id } + reaction: { type: reaction_type, user_id: user_id }, } body[:enforce_unique] = true if enforce_unique @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/reaction", body: body) @@ -39,7 +44,7 @@ def delete_reaction(message_id, reaction_type, user_id) @client.make_request( :delete, "/api/v2/chat/messages/#{message_id}/reaction/#{reaction_type}", - query_params: { 'user_id' => user_id } + query_params: { 'user_id' => user_id }, ) end @@ -48,68 +53,85 @@ def delete_reaction(message_id, reaction_type, user_id) # --------------------------------------------------------------------------- describe 'SendAndGetReactions' do + it 'sends reactions and gets them back' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1, @user2]) - msg_id = send_test_message('messaging', channel_id, @user1, "React to this #{SecureRandom.hex(8)}") + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + msg_id = send_test_message('messaging', channel_id, @user_1, "React to this #{SecureRandom.hex(8)}") # Send two reactions from different users - resp1 = send_reaction(msg_id, 'like', @user1) - expect(resp1.reaction).not_to be_nil - expect(resp1.reaction.to_h['type']).to eq('like') - expect(resp1.reaction.to_h['user_id']).to eq(@user1) + resp_1 = send_reaction(msg_id, 'like', @user_1) + expect(resp_1.reaction).not_to be_nil + expect(resp_1.reaction.to_h['type']).to eq('like') + expect(resp_1.reaction.to_h['user_id']).to eq(@user_1) - resp2 = send_reaction(msg_id, 'love', @user2) - expect(resp2.reaction).not_to be_nil - expect(resp2.reaction.to_h['type']).to eq('love') - expect(resp2.reaction.to_h['user_id']).to eq(@user2) + resp_2 = send_reaction(msg_id, 'love', @user_2) + expect(resp_2.reaction).not_to be_nil + expect(resp_2.reaction.to_h['type']).to eq('love') + expect(resp_2.reaction.to_h['user_id']).to eq(@user_2) # Get reactions get_resp = get_reactions(msg_id) expect(get_resp.reactions).not_to be_nil expect(get_resp.reactions.length).to be >= 2 + end + end describe 'DeleteReaction' do + it 'sends a reaction, deletes it, and verifies removal' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, "Delete reaction test #{SecureRandom.hex(8)}") + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, "Delete reaction test #{SecureRandom.hex(8)}") # Send reaction - send_reaction(msg_id, 'like', @user1) + send_reaction(msg_id, 'like', @user_1) # Delete reaction - del_resp = delete_reaction(msg_id, 'like', @user1) + del_resp = delete_reaction(msg_id, 'like', @user_1) expect(del_resp).not_to be_nil # Verify reaction is gone get_resp = get_reactions(msg_id) user_likes = (get_resp.reactions || []).select do |r| + h = r.is_a?(Hash) ? r : r.to_h - h['user_id'] == @user1 && h['type'] == 'like' + h['user_id'] == @user_1 && h['type'] == 'like' + end expect(user_likes.length).to eq(0) + end + end describe 'EnforceUniqueReaction' do + it 'enforces only one reaction per user when enforce_unique is set' do - _type, channel_id, _resp = create_test_channel_with_members(@user1, [@user1]) - msg_id = send_test_message('messaging', channel_id, @user1, "Unique reaction test #{SecureRandom.hex(8)}") + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, "Unique reaction test #{SecureRandom.hex(8)}") # Send first reaction with enforce_unique - send_reaction(msg_id, 'like', @user1, enforce_unique: true) + send_reaction(msg_id, 'like', @user_1, enforce_unique: true) # Send second reaction with enforce_unique — should replace, not duplicate - send_reaction(msg_id, 'love', @user1, enforce_unique: true) + send_reaction(msg_id, 'love', @user_1, enforce_unique: true) # Verify user has only one reaction get_resp = get_reactions(msg_id) user_reactions = (get_resp.reactions || []).select do |r| + h = r.is_a?(Hash) ? r : r.to_h - h['user_id'] == @user1 + h['user_id'] == @user_1 + end expect(user_reactions.length).to eq(1) + end + end + end diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb index 5943979..f939d5c 100644 --- a/spec/integration/chat_test_helpers.rb +++ b/spec/integration/chat_test_helpers.rb @@ -9,6 +9,7 @@ # Include this module in RSpec describe blocks and call `init_chat_client` # in a before(:all) hook. module ChatTestHelpers + # --------------------------------------------------------------------------- # Setup / teardown # --------------------------------------------------------------------------- @@ -23,14 +24,16 @@ def init_chat_client def cleanup_chat_resources # Delete channels first (they reference users) @created_channel_cids&.each do |cid| + type, id = cid.split(':', 2) @client.make_request( :delete, "/api/v2/chat/channels/#{type}/#{id}", - query_params: { 'hard_delete' => 'true' } + query_params: { 'hard_delete' => 'true' }, ) rescue StandardError => e puts "Warning: Failed to delete channel #{cid}: #{e.message}" + end # Delete users with retry @@ -41,27 +44,29 @@ def cleanup_chat_resources # Helper 1: random_string # --------------------------------------------------------------------------- - def random_string(n = 8) - SecureRandom.alphanumeric(n) + def random_string(length = 8) + SecureRandom.alphanumeric(length) end # --------------------------------------------------------------------------- # Helper 2: create_test_users # --------------------------------------------------------------------------- - def create_test_users(n) - ids = Array.new(n) { "test-user-#{SecureRandom.uuid}" } + def create_test_users(count) + ids = Array.new(count) { "test-user-#{SecureRandom.uuid}" } users = {} ids.each do |id| + users[id] = GetStream::Generated::Models::UserRequest.new( id: id, name: "Test User #{id[0..7]}", - role: 'user' + role: 'user', ) + end response = @client.common.update_users( - GetStream::Generated::Models::UpdateUsersRequest.new(users: users) + GetStream::Generated::Models::UpdateUsersRequest.new(users: users), ) @created_user_ids.concat(ids) [ids, response] @@ -77,7 +82,7 @@ def create_test_channel(creator_id) response = @client.make_request( :post, "/api/v2/chat/channels/messaging/#{channel_id}/query", - body: body + body: body, ) @created_channel_cids << "messaging:#{channel_id}" ['messaging', channel_id, response] @@ -94,7 +99,7 @@ def create_test_channel_with_members(creator_id, member_ids) response = @client.make_request( :post, "/api/v2/chat/channels/messaging/#{channel_id}/query", - body: body + body: body, ) @created_channel_cids << "messaging:#{channel_id}" ['messaging', channel_id, response] @@ -109,7 +114,7 @@ def send_test_message(channel_type, channel_id, user_id, text) resp = @client.make_request( :post, "/api/v2/chat/channels/#{channel_type}/#{channel_id}/message", - body: body + body: body, ) resp.message.id end @@ -120,19 +125,21 @@ def send_test_message(channel_type, channel_id, user_id, text) def delete_users_with_retry(user_ids) 10.times do |i| + @client.common.delete_users( GetStream::Generated::Models::DeleteUsersRequest.new( user_ids: user_ids, user: 'hard', messages: 'hard', - conversations: 'hard' - ) + conversations: 'hard', + ), ) - return + break rescue GetStreamRuby::APIError => e - return unless e.message.include?('Too many requests') + break unless e.message.include?('Too many requests') sleep([2**i, 16].min) + end end @@ -142,10 +149,12 @@ def delete_users_with_retry(user_ids) def wait_for_task(task_id, max_attempts: 30, interval_seconds: 1) max_attempts.times do + result = @client.common.get_task(task_id) return result if %w[completed failed].include?(result.status) sleep(interval_seconds) + end raise "Task #{task_id} did not complete after #{max_attempts} attempts" end @@ -170,4 +179,5 @@ def query_channels(body) def send_message(type, id, body) @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/message", body: body) end + end diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb index 306d7a0..60c0886 100644 --- a/spec/integration/chat_user_integration_spec.rb +++ b/spec/integration/chat_user_integration_spec.rb @@ -6,14 +6,19 @@ require_relative 'chat_test_helpers' RSpec.describe 'Chat User Integration', type: :integration do + include ChatTestHelpers before(:all) do + init_chat_client + end after(:all) do + cleanup_chat_resources + end # Helper to query users with a filter @@ -27,7 +32,9 @@ def query_users_with_filter(filter, **opts) end describe 'UpsertUsers' do + it 'creates 2 users and verifies both in response' do + user_ids, response = create_test_users(2) expect(response).to be_a(GetStreamRuby::StreamResponse) @@ -36,13 +43,19 @@ def query_users_with_filter(filter, **opts) users_hash = response.users expect(users_hash).not_to be_nil user_ids.each do |uid| + expect(users_hash.to_h.key?(uid)).to be true + end + end + end describe 'QueryUsers' do + it 'queries users with $in filter and verifies found' do + user_ids, _resp = create_test_users(2) resp = query_users_with_filter({ 'id' => { '$in' => user_ids } }) @@ -51,28 +64,38 @@ def query_users_with_filter(filter, **opts) returned_ids = resp.users.map { |u| u.to_h['id'] || u.id } user_ids.each do |uid| + expect(returned_ids).to include(uid) + end + end + end describe 'QueryUsersWithOffsetLimit' do + it 'queries with offset=1 limit=2 and verifies exactly 2 returned' do + user_ids, _resp = create_test_users(3) resp = query_users_with_filter( { 'id' => { '$in' => user_ids } }, offset: 1, limit: 2, - sort: [{ 'field' => 'id', 'direction' => 1 }] + sort: [{ 'field' => 'id', 'direction' => 1 }], ) expect(resp.users).not_to be_nil expect(resp.users.length).to eq(2) + end + end describe 'PartialUpdateUser' do + it 'sets custom fields then unsets one' do + user_ids, _resp = create_test_users(1) uid = user_ids.first @@ -82,10 +105,10 @@ def query_users_with_filter(filter, **opts) users: [ GetStream::Generated::Models::UpdateUserPartialRequest.new( id: uid, - set: { 'country' => 'NL', 'role' => 'admin' } - ) - ] - ) + set: { 'country' => 'NL', 'role' => 'admin' }, + ), + ], + ), ) # Verify set @@ -102,23 +125,27 @@ def query_users_with_filter(filter, **opts) users: [ GetStream::Generated::Models::UpdateUserPartialRequest.new( id: uid, - unset: ['country'] - ) - ] - ) + unset: ['country'], + ), + ], + ), ) # Verify unset - resp2 = query_users_with_filter({ 'id' => uid }) - user2 = resp2.users.first - user2_hash = user2.to_h - country2 = user2_hash['custom'].is_a?(Hash) ? user2_hash['custom']['country'] : user2_hash['country'] - expect(country2).to be_nil + resp_2 = query_users_with_filter({ 'id' => uid }) + user_2 = resp_2.users.first + user_2_hash = user_2.to_h + country_2 = user_2_hash['custom'].is_a?(Hash) ? user_2_hash['custom']['country'] : user_2_hash['country'] + expect(country_2).to be_nil + end + end describe 'BlockUnblockUser' do + it 'blocks user, verifies in blocked list, unblocks, verifies removed' do + user_ids, _resp = create_test_users(2) blocker_id = user_ids[0] blocked_id = user_ids[1] @@ -127,8 +154,8 @@ def query_users_with_filter(filter, **opts) @client.common.block_users( GetStream::Generated::Models::BlockUsersRequest.new( blocked_user_id: blocked_id, - user_id: blocker_id - ) + user_id: blocker_id, + ), ) # Verify blocked @@ -141,42 +168,50 @@ def query_users_with_filter(filter, **opts) @client.common.unblock_users( GetStream::Generated::Models::UnblockUsersRequest.new( blocked_user_id: blocked_id, - user_id: blocker_id - ) + user_id: blocker_id, + ), ) # Verify unblocked - blocked_resp2 = @client.common.get_blocked_users(blocker_id) - blocked_user_ids2 = (blocked_resp2.blocks || []).map { |b| b.to_h['blocked_user_id'] || b.blocked_user_id } - expect(blocked_user_ids2).not_to include(blocked_id) + blocked_resp_2 = @client.common.get_blocked_users(blocker_id) + blocked_user_ids_2 = (blocked_resp_2.blocks || []).map { |b| b.to_h['blocked_user_id'] || b.blocked_user_id } + expect(blocked_user_ids_2).not_to include(blocked_id) + end + end describe 'DeactivateReactivateUser' do + it 'deactivates then reactivates a user' do + user_ids, _resp = create_test_users(1) uid = user_ids.first # Deactivate @client.common.deactivate_user( uid, - GetStream::Generated::Models::DeactivateUserRequest.new + GetStream::Generated::Models::DeactivateUserRequest.new, ) # Reactivate @client.common.reactivate_user( uid, - GetStream::Generated::Models::ReactivateUserRequest.new + GetStream::Generated::Models::ReactivateUserRequest.new, ) # Verify active by querying resp = query_users_with_filter({ 'id' => uid }) expect(resp.users.length).to eq(1) + end + end describe 'DeleteUsers' do + it 'deletes 2 users with retry and polls task until completed' do + user_ids, _resp = create_test_users(2) # Remove from tracked list so cleanup doesn't double-delete @@ -187,19 +222,21 @@ def query_users_with_filter(filter, **opts) # wasting rate-limit tokens on rapid 429 responses. resp = nil 6.times do |i| + resp = @client.common.delete_users( GetStream::Generated::Models::DeleteUsersRequest.new( user_ids: user_ids, user: 'hard', messages: 'hard', - conversations: 'hard' - ) + conversations: 'hard', + ), ) break rescue GetStreamRuby::APIError => e raise unless e.message.include?('Too many requests') - sleep([5 * 2**i, 60].min) + sleep([5 * (2**i), 60].min) + end expect(resp).not_to be_nil @@ -208,30 +245,38 @@ def query_users_with_filter(filter, **opts) result = wait_for_task(task_id) expect(result.status).to eq('completed') + end + end describe 'ExportUser' do + it 'exports a user and verifies response not nil' do + user_ids, _resp = create_test_users(1) uid = user_ids.first resp = @client.common.export_user(uid) expect(resp).not_to be_nil + end + end describe 'CreateGuest' do + it 'creates guest and verifies access token' do + guest_id = "test-guest-#{SecureRandom.uuid}" resp = @client.common.create_guest( GetStream::Generated::Models::CreateGuestRequest.new( user: GetStream::Generated::Models::UserRequest.new( id: guest_id, - name: 'Test Guest' - ) - ) + name: 'Test Guest', + ), + ), ) expect(resp.access_token).not_to be_nil @@ -242,11 +287,15 @@ def query_users_with_filter(filter, **opts) rescue GetStreamRuby::APIError => e skip('Guest access not enabled') if e.message.downcase.include?('guest') raise + end + end describe 'UpsertUsersWithRoleAndTeamsRole' do + it 'creates user with role=admin, teams, and teams_role' do + uid = "test-user-#{SecureRandom.uuid}" @created_user_ids << uid @@ -258,10 +307,10 @@ def query_users_with_filter(filter, **opts) name: "Admin User #{uid[0..7]}", role: 'admin', teams: ['blue'], - teams_role: { 'blue' => 'admin' } - ) - } - ) + teams_role: { 'blue' => 'admin' }, + ), + }, + ), ) resp = query_users_with_filter({ 'id' => uid }) @@ -270,11 +319,15 @@ def query_users_with_filter(filter, **opts) expect(user_h['role']).to eq('admin') expect(user_h['teams']).to include('blue') expect(user_h['teams_role']).to eq({ 'blue' => 'admin' }) + end + end describe 'PartialUpdateUserWithTeam' do + it 'partial updates to add teams and teams_role' do + user_ids, _resp = create_test_users(1) uid = user_ids.first @@ -285,11 +338,11 @@ def query_users_with_filter(filter, **opts) id: uid, set: { 'teams' => ['blue'], - 'teams_role' => { 'blue' => 'admin' } - } - ) - ] - ) + 'teams_role' => { 'blue' => 'admin' }, + }, + ), + ], + ), ) resp = query_users_with_filter({ 'id' => uid }) @@ -297,11 +350,15 @@ def query_users_with_filter(filter, **opts) user_h = user.to_h expect(user_h['teams']).to include('blue') expect(user_h['teams_role']).to eq({ 'blue' => 'admin' }) + end + end describe 'UpdatePrivacySettings' do + it 'sets typing_indicators disabled then sets both typing + read_receipts' do + uid = "test-user-#{SecureRandom.uuid}" @created_user_ids << uid @@ -313,11 +370,11 @@ def query_users_with_filter(filter, **opts) id: uid, name: "Privacy User #{uid[0..7]}", privacy_settings: GetStream::Generated::Models::PrivacySettingsResponse.new( - typing_indicators: GetStream::Generated::Models::TypingIndicatorsResponse.new(enabled: false) - ) - ) - } - ) + typing_indicators: GetStream::Generated::Models::TypingIndicatorsResponse.new(enabled: false), + ), + ), + }, + ), ) resp = query_users_with_filter({ 'id' => uid }) @@ -332,22 +389,26 @@ def query_users_with_filter(filter, **opts) id: uid, privacy_settings: GetStream::Generated::Models::PrivacySettingsResponse.new( typing_indicators: GetStream::Generated::Models::TypingIndicatorsResponse.new(enabled: true), - read_receipts: GetStream::Generated::Models::ReadReceiptsResponse.new(enabled: false) - ) - ) - } - ) + read_receipts: GetStream::Generated::Models::ReadReceiptsResponse.new(enabled: false), + ), + ), + }, + ), ) - resp2 = query_users_with_filter({ 'id' => uid }) - user_h2 = resp2.users.first.to_h - expect(user_h2.dig('privacy_settings', 'typing_indicators', 'enabled')).to eq(true) - expect(user_h2.dig('privacy_settings', 'read_receipts', 'enabled')).to eq(false) + resp_2 = query_users_with_filter({ 'id' => uid }) + user_h_2 = resp_2.users.first.to_h + expect(user_h_2.dig('privacy_settings', 'typing_indicators', 'enabled')).to eq(true) + expect(user_h_2.dig('privacy_settings', 'read_receipts', 'enabled')).to eq(false) + end + end describe 'PartialUpdatePrivacySettings' do + it 'partial updates privacy settings incrementally' do + user_ids, _resp = create_test_users(1) uid = user_ids.first @@ -359,12 +420,12 @@ def query_users_with_filter(filter, **opts) id: uid, set: { 'privacy_settings' => { - 'typing_indicators' => { 'enabled' => true } - } - } - ) - ] - ) + 'typing_indicators' => { 'enabled' => true }, + }, + }, + ), + ], + ), ) resp = query_users_with_filter({ 'id' => uid }) @@ -379,29 +440,33 @@ def query_users_with_filter(filter, **opts) id: uid, set: { 'privacy_settings' => { - 'read_receipts' => { 'enabled' => false } - } - } - ) - ] - ) + 'read_receipts' => { 'enabled' => false }, + }, + }, + ), + ], + ), ) - resp2 = query_users_with_filter({ 'id' => uid }) - user_h2 = resp2.users.first.to_h - expect(user_h2.dig('privacy_settings', 'read_receipts', 'enabled')).to eq(false) + resp_2 = query_users_with_filter({ 'id' => uid }) + user_h_2 = resp_2.users.first.to_h + expect(user_h_2.dig('privacy_settings', 'read_receipts', 'enabled')).to eq(false) + end + end describe 'QueryUsersWithDeactivated' do + it 'deactivates one user, queries without/with include_deactivated' do + user_ids, _resp = create_test_users(3) deactivated_id = user_ids.first # Deactivate one user @client.common.deactivate_user( deactivated_id, - GetStream::Generated::Models::DeactivateUserRequest.new + GetStream::Generated::Models::DeactivateUserRequest.new, ) # Query WITHOUT include_deactivated_users — expect 2 @@ -409,28 +474,32 @@ def query_users_with_filter(filter, **opts) expect(resp.users.length).to eq(2) # Query WITH include_deactivated_users — expect 3 - resp2 = query_users_with_filter( + resp_2 = query_users_with_filter( { 'id' => { '$in' => user_ids } }, - include_deactivated_users: true + include_deactivated_users: true, ) - expect(resp2.users.length).to eq(3) + expect(resp_2.users.length).to eq(3) # Reactivate for cleanup @client.common.reactivate_user( deactivated_id, - GetStream::Generated::Models::ReactivateUserRequest.new + GetStream::Generated::Models::ReactivateUserRequest.new, ) + end + end describe 'DeactivateUsersPlural' do + it 'deactivates multiple users at once via async task' do + user_ids, _resp = create_test_users(2) resp = @client.common.deactivate_users( GetStream::Generated::Models::DeactivateUsersRequest.new( - user_ids: user_ids - ) + user_ids: user_ids, + ), ) task_id = resp.task_id @@ -445,20 +514,26 @@ def query_users_with_filter(filter, **opts) # Reactivate for cleanup user_ids.each do |uid| + @client.common.reactivate_user(uid, GetStream::Generated::Models::ReactivateUserRequest.new) + end + end + end describe 'UserCustomData' do + it 'creates user with custom fields and verifies persistence' do + uid = "test-user-#{SecureRandom.uuid}" @created_user_ids << uid custom_data = { 'favorite_color' => 'blue', 'age' => 30, - 'tags' => %w[vip early_adopter] + 'tags' => %w[vip early_adopter], } resp = @client.common.update_users( @@ -467,10 +542,10 @@ def query_users_with_filter(filter, **opts) uid => GetStream::Generated::Models::UserRequest.new( id: uid, name: "Custom User #{uid[0..7]}", - custom: custom_data - ) - } - ) + custom: custom_data, + ), + }, + ), ) # Verify in upsert response @@ -481,6 +556,9 @@ def query_users_with_filter(filter, **opts) query_resp = query_users_with_filter({ 'id' => uid }) user_h = query_resp.users.first.to_h expect(user_h['custom']['favorite_color'] || user_h['favorite_color']).to eq('blue') + end + end + end diff --git a/spec/integration/video_integration_spec.rb b/spec/integration/video_integration_spec.rb index 804d236..748b33a 100644 --- a/spec/integration/video_integration_spec.rb +++ b/spec/integration/video_integration_spec.rb @@ -6,39 +6,48 @@ require_relative 'chat_test_helpers' RSpec.describe 'Video Integration', type: :integration do + include ChatTestHelpers before(:all) do + init_chat_client @created_call_type_names = [] @created_call_ids = [] # [call_type, call_id] pairs @shared_user_ids, _resp = create_test_users(4) - @user1 = @shared_user_ids[0] - @user2 = @shared_user_ids[1] - @user3 = @shared_user_ids[2] - @user4 = @shared_user_ids[3] + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @user_3 = @shared_user_ids[2] + @user_4 = @shared_user_ids[3] + end after(:all) do + # Clean up calls (soft delete) @created_call_ids&.each do |call_type, call_id| + @client.make_request( :post, "/api/v2/video/call/#{call_type}/#{call_id}/delete", - body: {} + body: {}, ) rescue StandardError => e puts "Warning: Failed to delete call #{call_type}:#{call_id}: #{e.message}" + end # Clean up call types @created_call_type_names&.each do |name| + @client.make_request(:delete, "/api/v2/video/calltypes/#{name}") rescue StandardError => e puts "Warning: Failed to delete call type #{name}: #{e.message}" + end cleanup_chat_resources + end # --------------------------------------------------------------------------- @@ -74,87 +83,99 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'CRUDCallTypeOperations' do + it 'creates a call type with settings, updates, reads, and deletes' do + ct_name = new_call_type_name @created_call_type_names << ct_name # Create call type resp = @client.make_request(:post, '/api/v2/video/calltypes', body: { - name: ct_name, - grants: { - 'admin' => %w[send-audio send-video mute-users], - 'user' => %w[send-audio send-video] - }, - settings: { - audio: { default_device: 'speaker', mic_default_on: true }, - screensharing: { access_request_enabled: false, enabled: true } - }, - notification_settings: { - enabled: true, - call_notification: { - enabled: true, - apns: { title: '{{ user.display_name }} invites you to a call', body: '' } - }, - session_started: { enabled: false }, - call_live_started: { enabled: false }, - call_ring: { enabled: false } - } - }) + name: ct_name, + grants: { + 'admin' => %w[send-audio send-video mute-users], + 'user' => %w[send-audio send-video], + }, + settings: { + audio: { default_device: 'speaker', mic_default_on: true }, + screensharing: { access_request_enabled: false, enabled: true }, + }, + notification_settings: { + enabled: true, + call_notification: { + enabled: true, + apns: { title: '{{ user.display_name }} invites you to a call', body: '' }, + }, + session_started: { enabled: false }, + call_live_started: { enabled: false }, + call_ring: { enabled: false }, + }, + }) expect(resp.name).to eq(ct_name) # Poll for eventual consistency 10.times do + @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") break rescue GetStreamRuby::APIError sleep(1) + end # Update call type settings (with retry for eventual consistency) - resp2 = nil + resp_2 = nil 3.times do |i| - resp2 = @client.make_request(:put, "/api/v2/video/calltypes/#{ct_name}", body: { - settings: { - audio: { default_device: 'earpiece', mic_default_on: false }, - recording: { mode: 'disabled' }, - backstage: { enabled: true } - }, - grants: { - 'host' => %w[join-backstage] - } - }) + + resp_2 = @client.make_request(:put, "/api/v2/video/calltypes/#{ct_name}", body: { + settings: { + audio: { default_device: 'earpiece', mic_default_on: false }, + recording: { mode: 'disabled' }, + backstage: { enabled: true }, + }, + grants: { + 'host' => %w[join-backstage], + }, + }) break rescue GetStreamRuby::APIError raise if i == 2 sleep(2) + end - expect(resp2).not_to be_nil + expect(resp_2).not_to be_nil # Read call type (with retry) - resp3 = nil + resp_3 = nil 3.times do |i| - resp3 = @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") + + resp_3 = @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") break rescue GetStreamRuby::APIError raise if i == 2 sleep(2) + end - expect(resp3.name).to eq(ct_name) + expect(resp_3.name).to eq(ct_name) # Delete call type (with retry for eventual consistency) sleep(2) 5.times do |i| + @client.make_request(:delete, "/api/v2/video/calltypes/#{ct_name}") @created_call_type_names.delete(ct_name) break - rescue GetStreamRuby::APIError => e + rescue GetStreamRuby::APIError raise if i == 4 sleep(2) + end + end + end # --------------------------------------------------------------------------- @@ -162,23 +183,27 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'CreateCallWithMembers' do + it 'creates a call and adds members' do + call_id = new_call_id @created_call_ids << ['default', call_id] resp = create_call('default', call_id, { - data: { - created_by_id: @user1, - members: [ - { user_id: @user1 }, - { user_id: @user2 } - ] - } - }) + data: { + created_by_id: @user_1, + members: [ + { user_id: @user_1 }, + { user_id: @user_2 }, + ], + }, + }) expect(resp).not_to be_nil call_h = resp.to_h expect(call_h['call']).not_to be_nil + end + end # --------------------------------------------------------------------------- @@ -186,48 +211,54 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'BlockUnblockUserFromCalls' do + it 'blocks a user from a call and unblocks' do + call_id = new_call_id @created_call_ids << ['default', call_id] create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) # Block user @client.make_request( :post, "/api/v2/video/call/default/#{call_id}/block", - body: { user_id: @user2 } + body: { user_id: @user_2 }, ) # Verify blocked resp = get_call('default', call_id) call_h = resp.to_h blocked_ids = call_h.dig('call', 'blocked_user_ids') || [] - expect(blocked_ids).to include(@user2) + expect(blocked_ids).to include(@user_2) # Unblock user @client.make_request( :post, "/api/v2/video/call/default/#{call_id}/unblock", - body: { user_id: @user2 } + body: { user_id: @user_2 }, ) # Verify unblocked (with retry for eventual consistency) unblocked = false 5.times do + sleep(1) - resp2 = get_call('default', call_id) - call_h2 = resp2.to_h - blocked_ids2 = call_h2.dig('call', 'blocked_user_ids') || [] - unless blocked_ids2.include?(@user2) + resp_2 = get_call('default', call_id) + call_h_2 = resp_2.to_h + blocked_ids_2 = call_h_2.dig('call', 'blocked_user_ids') || [] + unless blocked_ids_2.include?(@user_2) unblocked = true break end + end expect(unblocked).to be(true), 'Expected user to be unblocked after unblock call' + end + end # --------------------------------------------------------------------------- @@ -235,21 +266,25 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'SendCustomEvent' do + it 'sends a custom event in a call' do + call_id = new_call_id @created_call_ids << ['default', call_id] create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) resp = @client.make_request( :post, "/api/v2/video/call/default/#{call_id}/event", - body: { user_id: @user1, custom: { bananas: 'good' } } + body: { user_id: @user_1, custom: { bananas: 'good' } }, ) expect(resp).not_to be_nil + end + end # --------------------------------------------------------------------------- @@ -257,25 +292,29 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'MuteAll' do + it 'mutes all users in a call' do + call_id = new_call_id @created_call_ids << ['default', call_id] create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) resp = @client.make_request( :post, "/api/v2/video/call/default/#{call_id}/mute_users", body: { - muted_by_id: @user1, + muted_by_id: @user_1, mute_all_users: true, - audio: true - } + audio: true, + }, ) expect(resp).not_to be_nil + end + end # --------------------------------------------------------------------------- @@ -283,28 +322,32 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'MuteSomeUsers' do + it 'mutes specific users with audio, video, screenshare' do + call_id = new_call_id @created_call_ids << ['default', call_id] create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) resp = @client.make_request( :post, "/api/v2/video/call/default/#{call_id}/mute_users", body: { - muted_by_id: @user1, - user_ids: [@user2, @user3], + muted_by_id: @user_1, + user_ids: [@user_2, @user_3], audio: true, video: true, screenshare: true, - screenshare_audio: true - } + screenshare_audio: true, + }, ) expect(resp).not_to be_nil + end + end # --------------------------------------------------------------------------- @@ -312,36 +355,40 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'UpdateUserPermissions' do + it 'revokes and grants permissions in a call' do + call_id = new_call_id @created_call_ids << ['default', call_id] create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) # Revoke send-audio - resp1 = @client.make_request( + resp_1 = @client.make_request( :post, "/api/v2/video/call/default/#{call_id}/user_permissions", body: { - user_id: @user2, - revoke_permissions: ['send-audio'] - } + user_id: @user_2, + revoke_permissions: ['send-audio'], + }, ) - expect(resp1).not_to be_nil + expect(resp_1).not_to be_nil # Grant send-audio back - resp2 = @client.make_request( + resp_2 = @client.make_request( :post, "/api/v2/video/call/default/#{call_id}/user_permissions", body: { - user_id: @user2, - grant_permissions: ['send-audio'] - } + user_id: @user_2, + grant_permissions: ['send-audio'], + }, ) - expect(resp2).not_to be_nil + expect(resp_2).not_to be_nil + end + end # --------------------------------------------------------------------------- @@ -349,7 +396,9 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'DeactivateUser' do + it 'deactivates, reactivates, and batch deactivates users' do + user_ids, _resp = create_test_users(2) alice = user_ids[0] bob = user_ids[1] @@ -357,24 +406,26 @@ def new_call_type_name # Deactivate single user @client.common.deactivate_user( alice, - GetStream::Generated::Models::DeactivateUserRequest.new + GetStream::Generated::Models::DeactivateUserRequest.new, ) # Reactivate single user @client.common.reactivate_user( alice, - GetStream::Generated::Models::ReactivateUserRequest.new + GetStream::Generated::Models::ReactivateUserRequest.new, ) # Batch deactivate resp = @client.common.deactivate_users( - GetStream::Generated::Models::DeactivateUsersRequest.new(user_ids: [alice, bob]) + GetStream::Generated::Models::DeactivateUsersRequest.new(user_ids: [alice, bob]), ) expect(resp.task_id).not_to be_nil task_result = wait_for_task(resp.task_id) expect(task_result.status).to eq('completed') + end + end # --------------------------------------------------------------------------- @@ -382,42 +433,46 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'CreateCallWithSessionTimer' do + it 'creates a call with max_duration_seconds and updates it' do + call_id = new_call_id @created_call_ids << ['default', call_id] resp = create_call('default', call_id, { - data: { - created_by_id: @user1, - settings_override: { - limits: { max_duration_seconds: 3600 } - } - } - }) + data: { + created_by_id: @user_1, + settings_override: { + limits: { max_duration_seconds: 3600 }, + }, + }, + }) call_h = resp.to_h max_dur = call_h.dig('call', 'settings', 'limits', 'max_duration_seconds') expect(max_dur).to eq(3600) # Update to 7200 - resp2 = update_call('default', call_id, { - settings_override: { - limits: { max_duration_seconds: 7200 } - } - }) - call_h2 = resp2.to_h - max_dur2 = call_h2.dig('call', 'settings', 'limits', 'max_duration_seconds') - expect(max_dur2).to eq(7200) + resp_2 = update_call('default', call_id, { + settings_override: { + limits: { max_duration_seconds: 7200 }, + }, + }) + call_h_2 = resp_2.to_h + max_dur_2 = call_h_2.dig('call', 'settings', 'limits', 'max_duration_seconds') + expect(max_dur_2).to eq(7200) # Reset to 0 - resp3 = update_call('default', call_id, { - settings_override: { - limits: { max_duration_seconds: 0 } - } - }) - call_h3 = resp3.to_h - max_dur3 = call_h3.dig('call', 'settings', 'limits', 'max_duration_seconds') - expect(max_dur3).to eq(0) + resp_3 = update_call('default', call_id, { + settings_override: { + limits: { max_duration_seconds: 0 }, + }, + }) + call_h_3 = resp_3.to_h + max_dur_3 = call_h_3.dig('call', 'settings', 'limits', 'max_duration_seconds') + expect(max_dur_3).to eq(0) + end + end # --------------------------------------------------------------------------- @@ -425,7 +480,9 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'UserBlocking' do + it 'blocks and unblocks a user at app level' do + user_ids, _resp = create_test_users(2) alice = user_ids[0] bob = user_ids[1] @@ -434,8 +491,8 @@ def new_call_type_name @client.common.block_users( GetStream::Generated::Models::BlockUsersRequest.new( blocked_user_id: bob, - user_id: alice - ) + user_id: alice, + ), ) # Verify blocked @@ -449,19 +506,23 @@ def new_call_type_name @client.common.unblock_users( GetStream::Generated::Models::UnblockUsersRequest.new( blocked_user_id: bob, - user_id: alice - ) + user_id: alice, + ), ) # Verify unblocked - resp2 = @client.common.get_blocked_users(alice) - blocks2 = resp2.blocks || [] - blocked_ids = blocks2.map do |b| + resp_2 = @client.common.get_blocked_users(alice) + blocks_2 = resp_2.blocks || [] + blocked_ids = blocks_2.map do |b| + h = b.is_a?(Hash) ? b : b.to_h h['blocked_user_id'] + end expect(blocked_ids).not_to include(bob) + end + end # --------------------------------------------------------------------------- @@ -469,45 +530,49 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'CreateCallWithBackstageAndJoinAhead' do + it 'creates a call with backstage and join_ahead_time_seconds' do + call_id = new_call_id @created_call_ids << ['default', call_id] - starts_at = (Time.now.utc + 30 * 60).strftime('%Y-%m-%dT%H:%M:%S.%NZ') + starts_at = (Time.now.utc + (30 * 60)).strftime('%Y-%m-%dT%H:%M:%S.%NZ') resp = create_call('default', call_id, { - data: { - starts_at: starts_at, - created_by_id: @user1, - settings_override: { - backstage: { enabled: true, join_ahead_time_seconds: 300 } - } - } - }) + data: { + starts_at: starts_at, + created_by_id: @user_1, + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 300 }, + }, + }, + }) call_h = resp.to_h join_ahead = call_h.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') expect(join_ahead).to eq(300) # Update to 600 - resp2 = update_call('default', call_id, { - settings_override: { - backstage: { enabled: true, join_ahead_time_seconds: 600 } - } - }) - call_h2 = resp2.to_h - join_ahead2 = call_h2.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') - expect(join_ahead2).to eq(600) + resp_2 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 600 }, + }, + }) + call_h_2 = resp_2.to_h + join_ahead_2 = call_h_2.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') + expect(join_ahead_2).to eq(600) # Reset to 0 - resp3 = update_call('default', call_id, { - settings_override: { - backstage: { enabled: true, join_ahead_time_seconds: 0 } - } - }) - call_h3 = resp3.to_h - join_ahead3 = call_h3.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') - expect(join_ahead3).to eq(0) + resp_3 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 0 }, + }, + }) + call_h_3 = resp_3.to_h + join_ahead_3 = call_h_3.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') + expect(join_ahead_3).to eq(0) + end + end # --------------------------------------------------------------------------- @@ -515,13 +580,15 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'DeleteCall (soft)' do + it 'soft deletes a call and verifies not found' do + call_id = new_call_id # Don't add to @created_call_ids since we're deleting it here create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) resp = delete_call_req('default', call_id, {}) resp_h = resp.to_h @@ -532,6 +599,7 @@ def new_call_type_name # Verify not found (with retry for eventual consistency) found = false 5.times do + sleep(1) begin get_call('default', call_id) @@ -539,9 +607,12 @@ def new_call_type_name found = true if e.message.include?("Can't find call with id") break end + end expect(found).to be(true), 'Expected call to be not found after soft delete' + end + end # --------------------------------------------------------------------------- @@ -549,13 +620,15 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'HardDeleteCall' do + it 'hard deletes a call with task polling' do + call_id = new_call_id # Don't add to @created_call_ids since we're deleting it here create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) resp = delete_call_req('default', call_id, { hard: true }) resp_h = resp.to_h @@ -568,6 +641,7 @@ def new_call_type_name # Verify not found (with retry for eventual consistency) found = false 5.times do + sleep(1) begin get_call('default', call_id) @@ -575,9 +649,12 @@ def new_call_type_name found = true if e.message.include?("Can't find call with id") break end + end expect(found).to be(true), 'Expected call to be not found after hard delete' + end + end # --------------------------------------------------------------------------- @@ -585,7 +662,9 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'Teams' do + it 'creates a user with teams, creates a call with team, queries' do + team_user_id = "test-user-#{SecureRandom.uuid}" @created_user_ids << team_user_id @@ -596,34 +675,36 @@ def new_call_type_name id: team_user_id, name: 'Team User', role: 'user', - teams: %w[red blue] - ) - } - ) + teams: %w[red blue], + ), + }, + ), ) call_id = new_call_id @created_call_ids << ['default', call_id] resp = create_call('default', call_id, { - data: { - created_by_id: team_user_id, - team: 'blue' - } - }) + data: { + created_by_id: team_user_id, + team: 'blue', + }, + }) call_h = resp.to_h expect(call_h.dig('call', 'team')).to eq('blue') # Query calls by team query_resp = @client.make_request(:post, '/api/v2/video/calls', body: { - filter_conditions: { - 'id' => call_id, - 'team' => { '$eq' => 'blue' } - } - }) + filter_conditions: { + 'id' => call_id, + 'team' => { '$eq' => 'blue' }, + }, + }) query_h = query_resp.to_h expect(query_h['calls'].length).to be >= 1 + end + end # --------------------------------------------------------------------------- @@ -631,26 +712,29 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'ExternalStorageOperations' do + it 'creates, lists, and deletes external storage' do + storage_name = "test-storage-#{random_string(10)}" # Create external storage (fake credentials for API contract testing only) create_resp = @client.make_request(:post, '/api/v2/external_storage', body: { - bucket: 'test-bucket', - name: storage_name, - storage_type: 's3', - path: 'test-directory/', - aws_s3: { - s3_region: 'us-east-1', - s3_api_key: 'test-access-key', - s3_secret: 'test-secret' - } - }) + bucket: 'test-bucket', + name: storage_name, + storage_type: 's3', + path: 'test-directory/', + 'aws_s3' => { + 's3_region' => 'us-east-1', + 's3_api_key' => 'test-access-key', + 's3_secret' => 'test-secret', + }, + }) expect(create_resp).not_to be_nil # Verify via list (with retry for eventual consistency) found = false 10.times do + sleep(1) list_resp = @client.make_request(:get, '/api/v2/external_storage') storages_h = list_resp.to_h['external_storages'] || {} @@ -658,19 +742,24 @@ def new_call_type_name found = true break end + end expect(found).to be(true), "Expected storage #{storage_name} to appear in list" # Delete external storage (with retry for eventual consistency) 5.times do |i| + @client.make_request(:delete, "/api/v2/external_storage/#{storage_name}") break - rescue GetStreamRuby::APIError => e + rescue GetStreamRuby::APIError raise if i == 4 sleep(2) + end + end + end # --------------------------------------------------------------------------- @@ -678,32 +767,36 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'EnableCallRecordingAndBackstageMode' do + it 'updates call settings for recording and backstage' do + call_id = new_call_id @created_call_ids << ['default', call_id] create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) # Enable recording - resp1 = update_call('default', call_id, { - settings_override: { - recording: { mode: 'available', audio_only: true } - } - }) - call_h1 = resp1.to_h - expect(call_h1.dig('call', 'settings', 'recording', 'mode')).to eq('available') + resp_1 = update_call('default', call_id, { + settings_override: { + recording: { mode: 'available', audio_only: true }, + }, + }) + call_h_1 = resp_1.to_h + expect(call_h_1.dig('call', 'settings', 'recording', 'mode')).to eq('available') # Enable backstage - resp2 = update_call('default', call_id, { - settings_override: { - backstage: { enabled: true } - } - }) - call_h2 = resp2.to_h - expect(call_h2.dig('call', 'settings', 'backstage', 'enabled')).to eq(true) + resp_2 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true }, + }, + }) + call_h_2 = resp_2.to_h + expect(call_h_2.dig('call', 'settings', 'backstage', 'enabled')).to eq(true) + end + end # --------------------------------------------------------------------------- @@ -711,36 +804,51 @@ def new_call_type_name # --------------------------------------------------------------------------- describe 'DeleteRecordingsAndTranscriptions' do + it 'returns error when deleting non-existent recording' do + call_id = new_call_id @created_call_ids << ['default', call_id] create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) + # rubocop:disable Layout/EmptyLinesAroundArguments expect do + @client.make_request( :delete, - "/api/v2/video/call/default/#{call_id}/non-existent-session/recordings/non-existent-filename" + "/api/v2/video/call/default/#{call_id}/non-existent-session/recordings/non-existent-filename", ) + end.to raise_error(GetStreamRuby::APIError) + # rubocop:enable Layout/EmptyLinesAroundArguments + end it 'returns error when deleting non-existent transcription' do + call_id = new_call_id @created_call_ids << ['default', call_id] create_call('default', call_id, { - data: { created_by_id: @user1 } - }) + data: { created_by_id: @user_1 }, + }) + # rubocop:disable Layout/EmptyLinesAroundArguments expect do + @client.make_request( :delete, - "/api/v2/video/call/default/#{call_id}/non-existent-session/transcriptions/non-existent-filename" + "/api/v2/video/call/default/#{call_id}/non-existent-session/transcriptions/non-existent-filename", ) + end.to raise_error(GetStreamRuby::APIError) + # rubocop:enable Layout/EmptyLinesAroundArguments + end + end + end From 3c75941414e80dbf63ca6736f4fedc73fd60e66d Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 17:14:49 +0100 Subject: [PATCH 12/29] test: re-get to verify updated channel --- spec/integration/chat_misc_integration_spec.rb | 18 +++++++++++------- spec/integration/chat_user_integration_spec.rb | 7 ++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb index 2b4e344..0d41b39 100644 --- a/spec/integration/chat_misc_integration_spec.rb +++ b/spec/integration/chat_misc_integration_spec.rb @@ -283,13 +283,17 @@ expect(get_resp.name).to eq(type_name) # Update channel type - update_resp = @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { - automod: 'disabled', - automod_behavior: 'flag', - max_message_length: 10_000, - typing_events: false, - }) - expect(update_resp.max_message_length).to eq(10_000) + @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 10_000, + typing_events: false, + }) + + # Re-fetch to verify (eventual consistency) + sleep(2) + updated = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") + expect(updated.max_message_length).to eq(10_000) # Delete a separate channel type del_name = "testdeltype#{random_string(6)}" diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb index 60c0886..94c823f 100644 --- a/spec/integration/chat_user_integration_spec.rb +++ b/spec/integration/chat_user_integration_spec.rb @@ -218,9 +218,10 @@ def query_users_with_filter(filter, **opts) user_ids.each { |uid| @created_user_ids.delete(uid) } # delete_users is heavily rate-limited; previous spec cleanups may have - # exhausted the budget. Use fewer retries with longer waits to avoid - # wasting rate-limit tokens on rapid 429 responses. + # exhausted the budget. Start with a longer initial wait and use + # exponential backoff to let the budget recover. resp = nil + sleep(5) # let rate-limit budget recover from prior cleanups 6.times do |i| resp = @client.common.delete_users( @@ -235,7 +236,7 @@ def query_users_with_filter(filter, **opts) rescue GetStreamRuby::APIError => e raise unless e.message.include?('Too many requests') - sleep([5 * (2**i), 60].min) + sleep([8 * (2**i), 60].min) end From 4bbb03e888b3c321906962c0e0f03055ddd89fd4 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 21:23:44 +0100 Subject: [PATCH 13/29] style: fix code formatting --- spec/integration/chat_message_integration_spec.rb | 4 ---- spec/integration/chat_moderation_integration_spec.rb | 10 +++++----- spec/integration/video_integration_spec.rb | 4 ---- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/spec/integration/chat_message_integration_spec.rb b/spec/integration/chat_message_integration_spec.rb index cbadd94..52220d5 100644 --- a/spec/integration/chat_message_integration_spec.rb +++ b/spec/integration/chat_message_integration_spec.rb @@ -624,7 +624,6 @@ def undelete_message(message_id, body) it 'verifies error when using both query and message_filter_conditions' do - # rubocop:disable Layout/EmptyLinesAroundArguments expect do search_messages( @@ -634,7 +633,6 @@ def undelete_message(message_id, body) ) end.to raise_error(GetStreamRuby::APIError) - # rubocop:enable Layout/EmptyLinesAroundArguments end @@ -665,7 +663,6 @@ def undelete_message(message_id, body) it 'verifies error when using offset with next' do - # rubocop:disable Layout/EmptyLinesAroundArguments expect do search_messages( @@ -676,7 +673,6 @@ def undelete_message(message_id, body) ) end.to raise_error(GetStreamRuby::APIError) - # rubocop:enable Layout/EmptyLinesAroundArguments end diff --git a/spec/integration/chat_moderation_integration_spec.rb b/spec/integration/chat_moderation_integration_spec.rb index dfc627f..7f290f0 100644 --- a/spec/integration/chat_moderation_integration_spec.rb +++ b/spec/integration/chat_moderation_integration_spec.rb @@ -62,7 +62,7 @@ h = b.is_a?(Hash) ? b : b.to_h target = h['user'] || {} - target = target.is_a?(Hash) ? target : target.to_h + target = target.to_h unless target.is_a?(Hash) target['id'] end @@ -87,7 +87,7 @@ h = b.is_a?(Hash) ? b : b.to_h target = h['user'] || {} - target = target.is_a?(Hash) ? target : target.to_h + target = target.to_h unless target.is_a?(Hash) target['id'] end @@ -157,7 +157,7 @@ mute_h = mute_resp.mutes[0].is_a?(Hash) ? mute_resp.mutes[0] : mute_resp.mutes[0].to_h target = mute_h['target'] || {} - target = target.is_a?(Hash) ? target : target.to_h + target = target.to_h unless target.is_a?(Hash) expect(target['id']).to eq(@user_4) # Verify via QueryUsers that muter has mutes @@ -174,7 +174,7 @@ t = m.is_a?(Hash) ? m : m.to_h tgt = t['target'] || {} - tgt = tgt.is_a?(Hash) ? tgt : tgt.to_h + tgt = tgt.to_h unless tgt.is_a?(Hash) tgt['id'] end @@ -198,7 +198,7 @@ t = m.is_a?(Hash) ? m : m.to_h tgt = t['target'] || {} - tgt = tgt.is_a?(Hash) ? tgt : tgt.to_h + tgt = tgt.to_h unless tgt.is_a?(Hash) tgt['id'] end diff --git a/spec/integration/video_integration_spec.rb b/spec/integration/video_integration_spec.rb index 748b33a..6d78511 100644 --- a/spec/integration/video_integration_spec.rb +++ b/spec/integration/video_integration_spec.rb @@ -814,7 +814,6 @@ def new_call_type_name data: { created_by_id: @user_1 }, }) - # rubocop:disable Layout/EmptyLinesAroundArguments expect do @client.make_request( @@ -823,7 +822,6 @@ def new_call_type_name ) end.to raise_error(GetStreamRuby::APIError) - # rubocop:enable Layout/EmptyLinesAroundArguments end @@ -836,7 +834,6 @@ def new_call_type_name data: { created_by_id: @user_1 }, }) - # rubocop:disable Layout/EmptyLinesAroundArguments expect do @client.make_request( @@ -845,7 +842,6 @@ def new_call_type_name ) end.to raise_error(GetStreamRuby::APIError) - # rubocop:enable Layout/EmptyLinesAroundArguments end From 8d8e7d3af612d16c5786d4246f3e909cd76a3b0f Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 21:51:42 +0100 Subject: [PATCH 14/29] test: fine tuning for api limits --- lib/getstream_ruby/client.rb | 1 + spec/integration/chat_test_helpers.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/getstream_ruby/client.rb b/lib/getstream_ruby/client.rb index 7d49495..32387c2 100644 --- a/lib/getstream_ruby/client.rb +++ b/lib/getstream_ruby/client.rb @@ -117,6 +117,7 @@ def build_connection interval: 0.05, interval_randomness: 0.5, backoff_factor: 2, + retry_statuses: [429], } conn.response :json, content_type: /\bjson$/ conn.adapter Faraday.default_adapter diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb index f939d5c..fdd576c 100644 --- a/spec/integration/chat_test_helpers.rb +++ b/spec/integration/chat_test_helpers.rb @@ -147,7 +147,7 @@ def delete_users_with_retry(user_ids) # Helper 7: wait_for_task # --------------------------------------------------------------------------- - def wait_for_task(task_id, max_attempts: 30, interval_seconds: 1) + def wait_for_task(task_id, max_attempts: 60, interval_seconds: 1) max_attempts.times do result = @client.common.get_task(task_id) From ef50f1cac4671de52f6df412f3c9ad645d6ec010 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 23:10:14 +0100 Subject: [PATCH 15/29] test: fine tuning --- lib/getstream_ruby/client.rb | 1 - spec/integration/chat_user_integration_spec.rb | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/getstream_ruby/client.rb b/lib/getstream_ruby/client.rb index 32387c2..7d49495 100644 --- a/lib/getstream_ruby/client.rb +++ b/lib/getstream_ruby/client.rb @@ -117,7 +117,6 @@ def build_connection interval: 0.05, interval_randomness: 0.5, backoff_factor: 2, - retry_statuses: [429], } conn.response :json, content_type: /\bjson$/ conn.adapter Faraday.default_adapter diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb index 94c823f..1d5c8ee 100644 --- a/spec/integration/chat_user_integration_spec.rb +++ b/spec/integration/chat_user_integration_spec.rb @@ -221,9 +221,11 @@ def query_users_with_filter(filter, **opts) # exhausted the budget. Start with a longer initial wait and use # exponential backoff to let the budget recover. resp = nil + last_error = nil sleep(5) # let rate-limit budget recover from prior cleanups 6.times do |i| + last_error = nil resp = @client.common.delete_users( GetStream::Generated::Models::DeleteUsersRequest.new( user_ids: user_ids, @@ -236,10 +238,13 @@ def query_users_with_filter(filter, **opts) rescue GetStreamRuby::APIError => e raise unless e.message.include?('Too many requests') + last_error = e sleep([8 * (2**i), 60].min) end + raise last_error if last_error + expect(resp).not_to be_nil task_id = resp.task_id expect(task_id).not_to be_nil From 4f030f92ffea687789dc940db06d0edd1221a673 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 25 Feb 2026 23:43:15 +0100 Subject: [PATCH 16/29] test: fine tuning --- spec/integration/chat_test_helpers.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb index fdd576c..28591be 100644 --- a/spec/integration/chat_test_helpers.rb +++ b/spec/integration/chat_test_helpers.rb @@ -36,8 +36,10 @@ def cleanup_chat_resources end - # Delete users with retry - delete_users_with_retry(@created_user_ids) if @created_user_ids && !@created_user_ids.empty? + # Users are intentionally not deleted here. The delete_users endpoint is + # heavily rate-limited; calling it from every spec file's cleanup exhausts + # the quota and causes the DeleteUsers integration test to fail. + # Test users have random UUIDs and do not interfere with other tests. end # --------------------------------------------------------------------------- From 78cbd34e1fc046294aacabfc8668034633e98fe6 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 08:58:29 +0100 Subject: [PATCH 17/29] test: fine tuning --- spec/integration/base_integration_test.rb | 1 + spec/integration/chat_test_helpers.rb | 38 +++-------- spec/integration/feed_integration_spec.rb | 30 +-------- spec/integration/suite_cleanup.rb | 82 +++++++++++++++++++++++ 4 files changed, 93 insertions(+), 58 deletions(-) create mode 100644 spec/integration/suite_cleanup.rb diff --git a/spec/integration/base_integration_test.rb b/spec/integration/base_integration_test.rb index cca3e90..90efac3 100644 --- a/spec/integration/base_integration_test.rb +++ b/spec/integration/base_integration_test.rb @@ -4,6 +4,7 @@ require 'securerandom' require 'dotenv' require_relative '../../lib/getstream_ruby' +require_relative 'suite_cleanup' # Base class for integration tests with common setup and cleanup class BaseIntegrationTest diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb index 28591be..f863bb1 100644 --- a/spec/integration/chat_test_helpers.rb +++ b/spec/integration/chat_test_helpers.rb @@ -4,6 +4,7 @@ require 'json' require 'dotenv' require_relative '../../lib/getstream_ruby' +require_relative 'suite_cleanup' # Shared helpers for chat integration tests. # Include this module in RSpec describe blocks and call `init_chat_client` @@ -22,7 +23,7 @@ def init_chat_client end def cleanup_chat_resources - # Delete channels first (they reference users) + # Delete channels (they reference users and must be removed per-spec). @created_channel_cids&.each do |cid| type, id = cid.split(':', 2) @@ -36,10 +37,11 @@ def cleanup_chat_resources end - # Users are intentionally not deleted here. The delete_users endpoint is - # heavily rate-limited; calling it from every spec file's cleanup exhausts - # the quota and causes the DeleteUsers integration test to fail. - # Test users have random UUIDs and do not interfere with other tests. + # Register users for deferred deletion at suite end. + # The delete_users endpoint is rate-limited; batching all user deletes into + # a single after(:suite) call (in suite_cleanup.rb) keeps the quota free + # for the DeleteUsers integration test that runs during the suite. + SuiteCleanup.register_users(@created_user_ids) end # --------------------------------------------------------------------------- @@ -122,31 +124,7 @@ def send_test_message(channel_type, channel_id, user_id, text) end # --------------------------------------------------------------------------- - # Helper 6: delete_users_with_retry - # --------------------------------------------------------------------------- - - def delete_users_with_retry(user_ids) - 10.times do |i| - - @client.common.delete_users( - GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: user_ids, - user: 'hard', - messages: 'hard', - conversations: 'hard', - ), - ) - break - rescue GetStreamRuby::APIError => e - break unless e.message.include?('Too many requests') - - sleep([2**i, 16].min) - - end - end - - # --------------------------------------------------------------------------- - # Helper 7: wait_for_task + # Helper 6: wait_for_task # --------------------------------------------------------------------------- def wait_for_task(task_id, max_attempts: 60, interval_seconds: 1) diff --git a/spec/integration/feed_integration_spec.rb b/spec/integration/feed_integration_spec.rb index 4bbeab1..9979820 100644 --- a/spec/integration/feed_integration_spec.rb +++ b/spec/integration/feed_integration_spec.rb @@ -311,20 +311,7 @@ puts "✅ Created/updated users in batch: #{user_id_1}, #{user_id_2}" # snippet-stop: UpdateUsers ensure - # Cleanup created users (with retry for rate limits) - 3.times do |i| - - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: [user_id_1, user_id_2], - user: 'hard', - ) - client.common.delete_users(delete_request) - break - rescue StandardError => e - puts "⚠️ Cleanup error: #{e.message}" if i == 2 - sleep(2**i) - - end + SuiteCleanup.register_users([user_id_1, user_id_2]) end end @@ -366,20 +353,7 @@ puts "✅ Partially updated user: #{user_id}" # snippet-stop: UpdateUsersPartial ensure - # Cleanup (with retry for rate limits) - 3.times do |i| - - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: [user_id], - user: 'hard', - ) - client.common.delete_users(delete_request) - break - rescue StandardError => e - puts "⚠️ Cleanup error: #{e.message}" if i == 2 - sleep(2**i) - - end + SuiteCleanup.register_users([user_id]) end end diff --git a/spec/integration/suite_cleanup.rb b/spec/integration/suite_cleanup.rb new file mode 100644 index 0000000..6551d00 --- /dev/null +++ b/spec/integration/suite_cleanup.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'dotenv' + +# Global registry that collects test user IDs across all spec files and +# deletes them in a single batched call after the full suite completes. +# +# Why: the delete_users endpoint is rate-limited. Calling it once per spec +# file (8+ calls per run) exhausts the quota and causes the DeleteUsers +# integration test to fail. Batching everything into one call at suite end +# reduces pressure to 1–2 API calls per run and keeps the test data clean. +# +# Usage: +# SuiteCleanup.register_users(user_ids) # call from any spec/helper +# +# The after(:suite) hook below triggers the actual deletion automatically. +module SuiteCleanup + + @user_ids = [] + + class << self + + def register_users(ids) + @user_ids.concat(Array(ids).compact) + end + + def run + return if @user_ids.empty? + + Dotenv.load('.env') if File.exist?('.env') + + # Require the library; it may already be loaded, require is idempotent. + require_relative '../../lib/getstream_ruby' + + # Allow network access in case WebMock disabled it after the last test. + WebMock.allow_net_connect! if defined?(WebMock) + + client = GetStreamRuby.client + uniq_ids = @user_ids.uniq + puts "\n🧹 Suite cleanup: deleting #{uniq_ids.length} test users..." + + # The delete_users endpoint accepts up to 100 user IDs per request. + uniq_ids.each_slice(100) do |batch| + + 3.times do |i| + + client.common.delete_users( + GetStream::Generated::Models::DeleteUsersRequest.new( + user_ids: batch, + user: 'hard', + messages: 'hard', + conversations: 'hard', + ), + ) + break + + rescue GetStreamRuby::APIError => e + + raise unless e.message.include?('Too many requests') + + wait = [30 * (2**i), 120].min + puts "⏳ Rate-limited during suite cleanup, retrying in #{wait}s..." + sleep(wait) + + end + + end + + puts '✅ Suite cleanup complete' + end + + end + +end + +RSpec.configure do |config| + + config.after(:suite) do + SuiteCleanup.run + end + +end From e6b67df4dd88bfeb3d47b9b406bccc5f1e52a5be Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 09:27:13 +0100 Subject: [PATCH 18/29] test: fine tuning for rate limiting --- .../integration/chat_user_integration_spec.rb | 13 +++--- spec/integration/feed_integration_spec.rb | 44 +++++++++---------- spec/integration/suite_cleanup.rb | 9 ++-- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb index 1d5c8ee..4e0cab1 100644 --- a/spec/integration/chat_user_integration_spec.rb +++ b/spec/integration/chat_user_integration_spec.rb @@ -217,13 +217,14 @@ def query_users_with_filter(filter, **opts) # Remove from tracked list so cleanup doesn't double-delete user_ids.each { |uid| @created_user_ids.delete(uid) } - # delete_users is heavily rate-limited; previous spec cleanups may have - # exhausted the budget. Start with a longer initial wait and use - # exponential backoff to let the budget recover. + # delete_users is rate-limited to 6 req/min on a fixed 1-minute clock + # window in the Stream backend. Crucially, rejected 429 calls still + # increment the counter, so exponential backoff makes things worse. + # Instead: on a 429, sleep until the next minute boundary (at most 61s) + # to guarantee a fresh window before retrying. resp = nil last_error = nil - sleep(5) # let rate-limit budget recover from prior cleanups - 6.times do |i| + 3.times do last_error = nil resp = @client.common.delete_users( @@ -239,7 +240,7 @@ def query_users_with_filter(filter, **opts) raise unless e.message.include?('Too many requests') last_error = e - sleep([8 * (2**i), 60].min) + sleep(61 - Time.now.sec) end diff --git a/spec/integration/feed_integration_spec.rb b/spec/integration/feed_integration_spec.rb index 9979820..3c8e9b9 100644 --- a/spec/integration/feed_integration_spec.rb +++ b/spec/integration/feed_integration_spec.rb @@ -386,41 +386,41 @@ client.common.update_users(create_request) # snippet-start: DeleteUsers - # Delete users in batch (with retry for rate limits) + # Delete users in batch. + # delete_users is rate-limited to 6 req/min on a fixed 1-minute clock + # window. Rejected 429 calls still increment the counter, so arbitrary + # backoff makes recovery harder. On a 429, sleep until the next minute + # boundary (at most 61s) to guarantee a fresh window before retrying. response = nil - 10.times do |i| - - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: user_ids, - user: 'hard', + last_error = nil + 3.times do + + last_error = nil + response = client.common.delete_users( + GetStream::Generated::Models::DeleteUsersRequest.new( + user_ids: user_ids, + user: 'hard', + ), ) - - response = client.common.delete_users(delete_request) break rescue GetStreamRuby::APIError => e raise unless e.message.include?('Too many requests') - sleep([2**i, 30].min) + last_error = e + sleep(61 - Time.now.sec) end + raise last_error if last_error + expect(response).not_to be_nil expect(response).to be_a(GetStreamRuby::StreamResponse) puts "✅ Deleted #{user_ids.length} users in batch" # snippet-stop: DeleteUsers - rescue StandardError => e - puts "⚠️ Error: #{e.message}" - # Try cleanup anyway - begin - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: user_ids, - user: 'hard', - ) - client.common.delete_users(delete_request) - rescue StandardError - # Ignore cleanup errors - end - raise e + ensure + # Register for suite cleanup. If delete_users already succeeded above, + # the suite cleanup attempt on these IDs is a harmless no-op. + SuiteCleanup.register_users(user_ids) end end diff --git a/spec/integration/suite_cleanup.rb b/spec/integration/suite_cleanup.rb index 6551d00..8b7ca3e 100644 --- a/spec/integration/suite_cleanup.rb +++ b/spec/integration/suite_cleanup.rb @@ -42,7 +42,7 @@ def run # The delete_users endpoint accepts up to 100 user IDs per request. uniq_ids.each_slice(100) do |batch| - 3.times do |i| + 3.times do client.common.delete_users( GetStream::Generated::Models::DeleteUsersRequest.new( @@ -58,8 +58,11 @@ def run raise unless e.message.include?('Too many requests') - wait = [30 * (2**i), 120].min - puts "⏳ Rate-limited during suite cleanup, retrying in #{wait}s..." + # The Stream backend enforces 6 req/min on a fixed 1-minute clock + # window. Sleep until the next boundary (at most 61s) to guarantee + # a fresh window. Arbitrary backoff risks retrying in the same window. + wait = 61 - Time.now.sec + puts "⏳ Rate-limited during suite cleanup, waiting #{wait}s for window reset..." sleep(wait) end From d0110d77073fa1976a0689f57c6aeedc883cbd90 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 09:42:18 +0100 Subject: [PATCH 19/29] test: fine tuning --- spec/integration/chat_misc_integration_spec.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb index 0d41b39..d3d4cd9 100644 --- a/spec/integration/chat_misc_integration_spec.rb +++ b/spec/integration/chat_misc_integration_spec.rb @@ -265,12 +265,14 @@ type_name = "testtype#{random_string(6)}" - # Create channel type + # Create channel type with a lower max_message_length so the update below + # can demonstrate the value actually changes. The test app plan caps at + # 5000, so stay within that ceiling to avoid silent truncation. create_resp = @client.make_request(:post, '/api/v2/chat/channeltypes', body: { name: type_name, automod: 'disabled', automod_behavior: 'flag', - max_message_length: 5000, + max_message_length: 4000, }) expect(create_resp.name).to eq(type_name) @created_channel_type_names << type_name @@ -282,18 +284,19 @@ get_resp = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") expect(get_resp.name).to eq(type_name) - # Update channel type + # Update channel type — raise to 5000 (plan maximum) to verify the + # update is applied and the new value is reflected on re-fetch. @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { automod: 'disabled', automod_behavior: 'flag', - max_message_length: 10_000, + max_message_length: 5000, typing_events: false, }) # Re-fetch to verify (eventual consistency) sleep(2) updated = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") - expect(updated.max_message_length).to eq(10_000) + expect(updated.max_message_length).to eq(5000) # Delete a separate channel type del_name = "testdeltype#{random_string(6)}" From 88605ce927e165028552ae0febcd84611fe06159 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 10:16:56 +0100 Subject: [PATCH 20/29] test: fine tuning --- spec/integration/suite_cleanup.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/integration/suite_cleanup.rb b/spec/integration/suite_cleanup.rb index 8b7ca3e..f4eb796 100644 --- a/spec/integration/suite_cleanup.rb +++ b/spec/integration/suite_cleanup.rb @@ -53,9 +53,7 @@ def run ), ) break - rescue GetStreamRuby::APIError => e - raise unless e.message.include?('Too many requests') # The Stream backend enforces 6 req/min on a fixed 1-minute clock @@ -79,7 +77,9 @@ def run RSpec.configure do |config| config.after(:suite) do + SuiteCleanup.run + end end From 49f166f7fadf051d68275e987fdde327b0382dcd Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 10:40:48 +0100 Subject: [PATCH 21/29] feat: update by openapi refactor --- lib/getstream_ruby/generated/common_client.rb | 165 ++++++++++++++++++ lib/getstream_ruby/generated/feeds_client.rb | 22 ++- .../models/add_user_group_members_request.rb | 36 ++++ .../models/add_user_group_members_response.rb | 36 ++++ .../generated/models/app_response_fields.rb | 10 ++ .../models/async_export_error_event.rb | 2 +- .../generated/models/aws_rekognition_rule.rb | 7 +- .../models/call_stats_report_ready_event.rb | 12 +- .../models/call_violation_count_parameters.rb | 36 ++++ .../generated/models/config_response.rb | 5 + .../generated/models/create_import_request.rb | 7 +- .../models/create_user_group_request.rb | 51 ++++++ .../models/create_user_group_response.rb | 36 ++++ .../generated/models/daily_value.rb | 36 ++++ .../models/delete_activity_request_payload.rb | 10 ++ .../models/delete_comment_request_payload.rb | 10 ++ .../models/delete_message_request_payload.rb | 10 ++ .../models/delete_reaction_request_payload.rb | 10 ++ .../models/delete_user_request_payload.rb | 10 ++ .../models/feed_group_restored_event.rb | 61 +++++++ .../models/get_user_group_response.rb | 36 ++++ .../models/import_v2_task_settings.rb | 5 + .../models/individual_record_settings.rb | 7 +- .../individual_recording_settings_request.rb | 7 +- .../individual_recording_settings_response.rb | 7 +- .../models/list_user_groups_response.rb | 36 ++++ .../generated/models/message_request.rb | 5 + .../generated/models/message_response.rb | 5 + .../models/message_with_channel_response.rb | 5 + .../generated/models/metric_stats.rb | 36 ++++ .../models/query_team_usage_stats_request.rb | 51 ++++++ .../models/query_team_usage_stats_response.rb | 41 +++++ .../models/read_collections_response.rb | 12 +- .../remove_user_group_members_response.rb | 36 ++++ .../models/restore_feed_group_request.rb | 14 ++ .../models/restore_feed_group_response.rb | 36 ++++ .../models/rule_builder_condition.rb | 5 + .../generated/models/search_result_message.rb | 5 + .../models/search_user_groups_response.rb | 36 ++++ .../generated/models/team_usage_stats.rb | 111 ++++++++++++ .../models/undelete_message_request.rb | 31 ++++ .../models/undelete_message_response.rb | 36 ++++ .../generated/models/update_app_request.rb | 5 + .../models/update_user_group_request.rb | 41 +++++ .../models/update_user_group_response.rb | 36 ++++ .../generated/models/user_group.rb | 71 ++++++++ .../models/user_group_created_event.rb | 56 ++++++ .../models/user_group_deleted_event.rb | 56 ++++++ .../generated/models/user_group_member.rb | 51 ++++++ .../models/user_group_member_added_event.rb | 61 +++++++ .../models/user_group_member_removed_event.rb | 61 +++++++ .../generated/models/user_group_response.rb | 66 +++++++ .../models/user_group_updated_event.rb | 56 ++++++ lib/getstream_ruby/generated/webhook.rb | 24 +++ test/webhook_test.rb | 30 ++++ 55 files changed, 1738 insertions(+), 11 deletions(-) create mode 100644 lib/getstream_ruby/generated/models/add_user_group_members_request.rb create mode 100644 lib/getstream_ruby/generated/models/add_user_group_members_response.rb create mode 100644 lib/getstream_ruby/generated/models/call_violation_count_parameters.rb create mode 100644 lib/getstream_ruby/generated/models/create_user_group_request.rb create mode 100644 lib/getstream_ruby/generated/models/create_user_group_response.rb create mode 100644 lib/getstream_ruby/generated/models/daily_value.rb create mode 100644 lib/getstream_ruby/generated/models/feed_group_restored_event.rb create mode 100644 lib/getstream_ruby/generated/models/get_user_group_response.rb create mode 100644 lib/getstream_ruby/generated/models/list_user_groups_response.rb create mode 100644 lib/getstream_ruby/generated/models/metric_stats.rb create mode 100644 lib/getstream_ruby/generated/models/query_team_usage_stats_request.rb create mode 100644 lib/getstream_ruby/generated/models/query_team_usage_stats_response.rb create mode 100644 lib/getstream_ruby/generated/models/remove_user_group_members_response.rb create mode 100644 lib/getstream_ruby/generated/models/restore_feed_group_request.rb create mode 100644 lib/getstream_ruby/generated/models/restore_feed_group_response.rb create mode 100644 lib/getstream_ruby/generated/models/search_user_groups_response.rb create mode 100644 lib/getstream_ruby/generated/models/team_usage_stats.rb create mode 100644 lib/getstream_ruby/generated/models/undelete_message_request.rb create mode 100644 lib/getstream_ruby/generated/models/undelete_message_response.rb create mode 100644 lib/getstream_ruby/generated/models/update_user_group_request.rb create mode 100644 lib/getstream_ruby/generated/models/update_user_group_response.rb create mode 100644 lib/getstream_ruby/generated/models/user_group.rb create mode 100644 lib/getstream_ruby/generated/models/user_group_created_event.rb create mode 100644 lib/getstream_ruby/generated/models/user_group_deleted_event.rb create mode 100644 lib/getstream_ruby/generated/models/user_group_member.rb create mode 100644 lib/getstream_ruby/generated/models/user_group_member_added_event.rb create mode 100644 lib/getstream_ruby/generated/models/user_group_member_removed_event.rb create mode 100644 lib/getstream_ruby/generated/models/user_group_response.rb create mode 100644 lib/getstream_ruby/generated/models/user_group_updated_event.rb diff --git a/lib/getstream_ruby/generated/common_client.rb b/lib/getstream_ruby/generated/common_client.rb index 475ab20..7e4d53e 100644 --- a/lib/getstream_ruby/generated/common_client.rb +++ b/lib/getstream_ruby/generated/common_client.rb @@ -1027,6 +1027,171 @@ def upload_image(image_upload_request) ) end + # Lists user groups with cursor-based pagination + # + # @param limit [Integer] + # @param id_gt [String] + # @param created_at_gt [String] + # @param team_id [String] + # @return [Models::ListUserGroupsResponse] + def list_user_groups(limit = nil, id_gt = nil, created_at_gt = nil, team_id = nil) + path = '/api/v2/usergroups' + # Build query parameters + query_params = {} + query_params['limit'] = limit unless limit.nil? + query_params['id_gt'] = id_gt unless id_gt.nil? + query_params['created_at_gt'] = created_at_gt unless created_at_gt.nil? + query_params['team_id'] = team_id unless team_id.nil? + + # Make the API request + @client.make_request( + :get, + path, + query_params: query_params + ) + end + + # Creates a new user group, optionally with initial members + # + # @param create_user_group_request [CreateUserGroupRequest] + # @return [Models::CreateUserGroupResponse] + def create_user_group(create_user_group_request) + path = '/api/v2/usergroups' + # Build request body + body = create_user_group_request + + # Make the API request + @client.make_request( + :post, + path, + body: body + ) + end + + # Searches user groups by name prefix for autocomplete + # + # @param query [String] + # @param limit [Integer] + # @param name_gt [String] + # @param id_gt [String] + # @param team_id [String] + # @return [Models::SearchUserGroupsResponse] + def search_user_groups(query, limit = nil, name_gt = nil, id_gt = nil, team_id = nil) + path = '/api/v2/usergroups/search' + # Build query parameters + query_params = {} + query_params['query'] = query unless query.nil? + query_params['limit'] = limit unless limit.nil? + query_params['name_gt'] = name_gt unless name_gt.nil? + query_params['id_gt'] = id_gt unless id_gt.nil? + query_params['team_id'] = team_id unless team_id.nil? + + # Make the API request + @client.make_request( + :get, + path, + query_params: query_params + ) + end + + # Deletes a user group and all its members + # + # @param _id [String] + # @param team_id [String] + # @return [Models::Response] + def delete_user_group(_id, team_id = nil) + path = '/api/v2/usergroups/{id}' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build query parameters + query_params = {} + query_params['team_id'] = team_id unless team_id.nil? + + # Make the API request + @client.make_request( + :delete, + path, + query_params: query_params + ) + end + + # Gets a user group by ID, including its members + # + # @param _id [String] + # @param team_id [String] + # @return [Models::GetUserGroupResponse] + def get_user_group(_id, team_id = nil) + path = '/api/v2/usergroups/{id}' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build query parameters + query_params = {} + query_params['team_id'] = team_id unless team_id.nil? + + # Make the API request + @client.make_request( + :get, + path, + query_params: query_params + ) + end + + # Updates a user group's name and/or description. team_id is immutable. + # + # @param _id [String] + # @param update_user_group_request [UpdateUserGroupRequest] + # @return [Models::UpdateUserGroupResponse] + def update_user_group(_id, update_user_group_request) + path = '/api/v2/usergroups/{id}' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build request body + body = update_user_group_request + + # Make the API request + @client.make_request( + :put, + path, + body: body + ) + end + + # Removes members from a user group. Users already not in the group are silently ignored. + # + # @param _id [String] + # @return [Models::RemoveUserGroupMembersResponse] + def remove_user_group_members(_id) + path = '/api/v2/usergroups/{id}/members' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + + # Make the API request + @client.make_request( + :delete, + path + ) + end + + # Adds members to a user group. All user IDs must exist. The operation is all-or-nothing. + # + # @param _id [String] + # @param add_user_group_members_request [AddUserGroupMembersRequest] + # @return [Models::AddUserGroupMembersResponse] + def add_user_group_members(_id, add_user_group_members_request) + path = '/api/v2/usergroups/{id}/members' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build request body + body = add_user_group_members_request + + # Make the API request + @client.make_request( + :post, + path, + body: body + ) + end + # Find and filter users # # @param payload [QueryUsersPayload] diff --git a/lib/getstream_ruby/generated/feeds_client.rb b/lib/getstream_ruby/generated/feeds_client.rb index 42285b7..412bab3 100644 --- a/lib/getstream_ruby/generated/feeds_client.rb +++ b/lib/getstream_ruby/generated/feeds_client.rb @@ -481,15 +481,15 @@ def delete_collections(collection_refs) # Read collections with optional filtering by user ID and collection name. By default, users can only read their own collections. # - # @param collection_refs [Array] # @param user_id [String] + # @param collection_refs [Array] # @return [Models::ReadCollectionsResponse] - def read_collections(collection_refs, user_id = nil) + def read_collections(user_id = nil, collection_refs = nil) path = '/api/v2/feeds/collections' # Build query parameters query_params = {} - query_params['collection_refs'] = collection_refs unless collection_refs.nil? query_params['user_id'] = user_id unless user_id.nil? + query_params['collection_refs'] = collection_refs unless collection_refs.nil? # Make the API request @client.make_request( @@ -1098,6 +1098,22 @@ def get_follow_suggestions(feed_group_id, limit = nil, user_id = nil) ) end + # Restores a soft-deleted feed group by its ID. Only clears DeletedAt in the database; no other fields are updated. + # + # @param feed_group_id [String] + # @return [Models::RestoreFeedGroupResponse] + def restore_feed_group(feed_group_id) + path = '/api/v2/feeds/feed_groups/{feed_group_id}/restore' + # Replace path parameters + path = path.gsub('{feed_group_id}', feed_group_id.to_s) + + # Make the API request + @client.make_request( + :post, + path + ) + end + # Delete a feed group by its ID. Can perform a soft delete (default) or hard delete. # # @param _id [String] diff --git a/lib/getstream_ruby/generated/models/add_user_group_members_request.rb b/lib/getstream_ruby/generated/models/add_user_group_members_request.rb new file mode 100644 index 0000000..544fc5a --- /dev/null +++ b/lib/getstream_ruby/generated/models/add_user_group_members_request.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request body for adding members to a user group + class AddUserGroupMembersRequest < GetStream::BaseModel + + # Model attributes + # @!attribute member_ids + # @return [Array] List of user IDs to add as members + attr_accessor :member_ids + # @!attribute team_id + # @return [String] + attr_accessor :team_id + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @member_ids = attributes[:member_ids] || attributes['member_ids'] + @team_id = attributes[:team_id] || attributes['team_id'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + member_ids: 'member_ids', + team_id: 'team_id' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/add_user_group_members_response.rb b/lib/getstream_ruby/generated/models/add_user_group_members_response.rb new file mode 100644 index 0000000..31f4bec --- /dev/null +++ b/lib/getstream_ruby/generated/models/add_user_group_members_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for adding members to a user group + class AddUserGroupMembersResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/app_response_fields.rb b/lib/getstream_ruby/generated/models/app_response_fields.rb index e11186f..ae078a2 100644 --- a/lib/getstream_ruby/generated/models/app_response_fields.rb +++ b/lib/getstream_ruby/generated/models/app_response_fields.rb @@ -48,6 +48,9 @@ class AppResponseFields < GetStream::BaseModel # @!attribute max_aggregated_activities_length # @return [Integer] attr_accessor :max_aggregated_activities_length + # @!attribute moderation_audio_call_moderation_enabled + # @return [Boolean] + attr_accessor :moderation_audio_call_moderation_enabled # @!attribute moderation_enabled # @return [Boolean] attr_accessor :moderation_enabled @@ -57,6 +60,9 @@ class AppResponseFields < GetStream::BaseModel # @!attribute moderation_multitenant_blocklist_enabled # @return [Boolean] attr_accessor :moderation_multitenant_blocklist_enabled + # @!attribute moderation_video_call_moderation_enabled + # @return [Boolean] + attr_accessor :moderation_video_call_moderation_enabled # @!attribute moderation_webhook_url # @return [String] attr_accessor :moderation_webhook_url @@ -179,9 +185,11 @@ def initialize(attributes = {}) @id = attributes[:id] || attributes['id'] @image_moderation_enabled = attributes[:image_moderation_enabled] || attributes['image_moderation_enabled'] @max_aggregated_activities_length = attributes[:max_aggregated_activities_length] || attributes['max_aggregated_activities_length'] + @moderation_audio_call_moderation_enabled = attributes[:moderation_audio_call_moderation_enabled] || attributes['moderation_audio_call_moderation_enabled'] @moderation_enabled = attributes[:moderation_enabled] || attributes['moderation_enabled'] @moderation_llm_configurability_enabled = attributes[:moderation_llm_configurability_enabled] || attributes['moderation_llm_configurability_enabled'] @moderation_multitenant_blocklist_enabled = attributes[:moderation_multitenant_blocklist_enabled] || attributes['moderation_multitenant_blocklist_enabled'] + @moderation_video_call_moderation_enabled = attributes[:moderation_video_call_moderation_enabled] || attributes['moderation_video_call_moderation_enabled'] @moderation_webhook_url = attributes[:moderation_webhook_url] || attributes['moderation_webhook_url'] @multi_tenant_enabled = attributes[:multi_tenant_enabled] || attributes['multi_tenant_enabled'] @name = attributes[:name] || attributes['name'] @@ -235,9 +243,11 @@ def self.json_field_mappings id: 'id', image_moderation_enabled: 'image_moderation_enabled', max_aggregated_activities_length: 'max_aggregated_activities_length', + moderation_audio_call_moderation_enabled: 'moderation_audio_call_moderation_enabled', moderation_enabled: 'moderation_enabled', moderation_llm_configurability_enabled: 'moderation_llm_configurability_enabled', moderation_multitenant_blocklist_enabled: 'moderation_multitenant_blocklist_enabled', + moderation_video_call_moderation_enabled: 'moderation_video_call_moderation_enabled', moderation_webhook_url: 'moderation_webhook_url', multi_tenant_enabled: 'multi_tenant_enabled', name: 'name', diff --git a/lib/getstream_ruby/generated/models/async_export_error_event.rb b/lib/getstream_ruby/generated/models/async_export_error_event.rb index e297b0e..1e922a8 100644 --- a/lib/getstream_ruby/generated/models/async_export_error_event.rb +++ b/lib/getstream_ruby/generated/models/async_export_error_event.rb @@ -43,7 +43,7 @@ def initialize(attributes = {}) @started_at = attributes[:started_at] || attributes['started_at'] @task_id = attributes[:task_id] || attributes['task_id'] @custom = attributes[:custom] || attributes['custom'] - @type = attributes[:type] || attributes['type'] || "export.channels.error" + @type = attributes[:type] || attributes['type'] || "export.users.error" @received_at = attributes[:received_at] || attributes['received_at'] || nil end diff --git a/lib/getstream_ruby/generated/models/aws_rekognition_rule.rb b/lib/getstream_ruby/generated/models/aws_rekognition_rule.rb index 93720c9..b00fe08 100644 --- a/lib/getstream_ruby/generated/models/aws_rekognition_rule.rb +++ b/lib/getstream_ruby/generated/models/aws_rekognition_rule.rb @@ -18,6 +18,9 @@ class AWSRekognitionRule < GetStream::BaseModel # @!attribute min_confidence # @return [Float] attr_accessor :min_confidence + # @!attribute subclassifications + # @return [Hash] + attr_accessor :subclassifications # Initialize with attributes def initialize(attributes = {}) @@ -25,6 +28,7 @@ def initialize(attributes = {}) @action = attributes[:action] || attributes['action'] @label = attributes[:label] || attributes['label'] @min_confidence = attributes[:min_confidence] || attributes['min_confidence'] + @subclassifications = attributes[:subclassifications] || attributes['subclassifications'] || nil end # Override field mappings for JSON serialization @@ -32,7 +36,8 @@ def self.json_field_mappings { action: 'action', label: 'label', - min_confidence: 'min_confidence' + min_confidence: 'min_confidence', + subclassifications: 'subclassifications' } end end diff --git a/lib/getstream_ruby/generated/models/call_stats_report_ready_event.rb b/lib/getstream_ruby/generated/models/call_stats_report_ready_event.rb index f3b0310..6a0c82c 100644 --- a/lib/getstream_ruby/generated/models/call_stats_report_ready_event.rb +++ b/lib/getstream_ruby/generated/models/call_stats_report_ready_event.rb @@ -21,6 +21,12 @@ class CallStatsReportReadyEvent < GetStream::BaseModel # @!attribute type # @return [String] The type of event, "call.report_ready" in this case attr_accessor :type + # @!attribute is_trimmed + # @return [Boolean] Whether participants_overview is truncated by the server-side limit + attr_accessor :is_trimmed + # @!attribute participants_overview + # @return [Array] Top participant sessions overview + attr_accessor :participants_overview # Initialize with attributes def initialize(attributes = {}) @@ -29,6 +35,8 @@ def initialize(attributes = {}) @created_at = attributes[:created_at] || attributes['created_at'] @session_id = attributes[:session_id] || attributes['session_id'] @type = attributes[:type] || attributes['type'] || "call.stats_report_ready" + @is_trimmed = attributes[:is_trimmed] || attributes['is_trimmed'] || nil + @participants_overview = attributes[:participants_overview] || attributes['participants_overview'] || nil end # Override field mappings for JSON serialization @@ -37,7 +45,9 @@ def self.json_field_mappings call_cid: 'call_cid', created_at: 'created_at', session_id: 'session_id', - type: 'type' + type: 'type', + is_trimmed: 'is_trimmed', + participants_overview: 'participants_overview' } end end diff --git a/lib/getstream_ruby/generated/models/call_violation_count_parameters.rb b/lib/getstream_ruby/generated/models/call_violation_count_parameters.rb new file mode 100644 index 0000000..78350b6 --- /dev/null +++ b/lib/getstream_ruby/generated/models/call_violation_count_parameters.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class CallViolationCountParameters < GetStream::BaseModel + + # Model attributes + # @!attribute threshold + # @return [Integer] + attr_accessor :threshold + # @!attribute time_window + # @return [String] + attr_accessor :time_window + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @threshold = attributes[:threshold] || attributes['threshold'] || nil + @time_window = attributes[:time_window] || attributes['time_window'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + threshold: 'threshold', + time_window: 'time_window' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/config_response.rb b/lib/getstream_ruby/generated/models/config_response.rb index f590f1a..c8c1668 100644 --- a/lib/getstream_ruby/generated/models/config_response.rb +++ b/lib/getstream_ruby/generated/models/config_response.rb @@ -30,6 +30,9 @@ class ConfigResponse < GetStream::BaseModel # @!attribute ai_image_config # @return [AIImageConfig] attr_accessor :ai_image_config + # @!attribute ai_image_subclassifications + # @return [Hash>] Available L2 subclassifications per L1 image moderation label, based on the active provider + attr_accessor :ai_image_subclassifications # @!attribute ai_text_config # @return [AITextConfig] attr_accessor :ai_text_config @@ -68,6 +71,7 @@ def initialize(attributes = {}) @updated_at = attributes[:updated_at] || attributes['updated_at'] @supported_video_call_harm_types = attributes[:supported_video_call_harm_types] || attributes['supported_video_call_harm_types'] @ai_image_config = attributes[:ai_image_config] || attributes['ai_image_config'] || nil + @ai_image_subclassifications = attributes[:ai_image_subclassifications] || attributes['ai_image_subclassifications'] || nil @ai_text_config = attributes[:ai_text_config] || attributes['ai_text_config'] || nil @ai_video_config = attributes[:ai_video_config] || attributes['ai_video_config'] || nil @automod_platform_circumvention_config = attributes[:automod_platform_circumvention_config] || attributes['automod_platform_circumvention_config'] || nil @@ -89,6 +93,7 @@ def self.json_field_mappings updated_at: 'updated_at', supported_video_call_harm_types: 'supported_video_call_harm_types', ai_image_config: 'ai_image_config', + ai_image_subclassifications: 'ai_image_subclassifications', ai_text_config: 'ai_text_config', ai_video_config: 'ai_video_config', automod_platform_circumvention_config: 'automod_platform_circumvention_config', diff --git a/lib/getstream_ruby/generated/models/create_import_request.rb b/lib/getstream_ruby/generated/models/create_import_request.rb index 5586f27..88849bb 100644 --- a/lib/getstream_ruby/generated/models/create_import_request.rb +++ b/lib/getstream_ruby/generated/models/create_import_request.rb @@ -15,19 +15,24 @@ class CreateImportRequest < GetStream::BaseModel # @!attribute path # @return [String] attr_accessor :path + # @!attribute merge_custom + # @return [Boolean] + attr_accessor :merge_custom # Initialize with attributes def initialize(attributes = {}) super(attributes) @mode = attributes[:mode] || attributes['mode'] @path = attributes[:path] || attributes['path'] + @merge_custom = attributes[:merge_custom] || attributes['merge_custom'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { mode: 'mode', - path: 'path' + path: 'path', + merge_custom: 'merge_custom' } end end diff --git a/lib/getstream_ruby/generated/models/create_user_group_request.rb b/lib/getstream_ruby/generated/models/create_user_group_request.rb new file mode 100644 index 0000000..cd00dee --- /dev/null +++ b/lib/getstream_ruby/generated/models/create_user_group_request.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request body for creating a user group + class CreateUserGroupRequest < GetStream::BaseModel + + # Model attributes + # @!attribute name + # @return [String] The user friendly name of the user group + attr_accessor :name + # @!attribute description + # @return [String] An optional description for the group + attr_accessor :description + # @!attribute id + # @return [String] Optional user group ID. If not provided, a UUID v7 will be generated + attr_accessor :id + # @!attribute team_id + # @return [String] Optional team ID to scope the group to a team + attr_accessor :team_id + # @!attribute member_ids + # @return [Array] Optional initial list of user IDs to add as members + attr_accessor :member_ids + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @name = attributes[:name] || attributes['name'] + @description = attributes[:description] || attributes['description'] || nil + @id = attributes[:id] || attributes['id'] || nil + @team_id = attributes[:team_id] || attributes['team_id'] || nil + @member_ids = attributes[:member_ids] || attributes['member_ids'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + name: 'name', + description: 'description', + id: 'id', + team_id: 'team_id', + member_ids: 'member_ids' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/create_user_group_response.rb b/lib/getstream_ruby/generated/models/create_user_group_response.rb new file mode 100644 index 0000000..4b664dc --- /dev/null +++ b/lib/getstream_ruby/generated/models/create_user_group_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for creating a user group + class CreateUserGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/daily_value.rb b/lib/getstream_ruby/generated/models/daily_value.rb new file mode 100644 index 0000000..297fd19 --- /dev/null +++ b/lib/getstream_ruby/generated/models/daily_value.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Metric value for a specific date + class DailyValue < GetStream::BaseModel + + # Model attributes + # @!attribute date + # @return [String] Date in YYYY-MM-DD format + attr_accessor :date + # @!attribute value + # @return [Integer] Metric value for this date + attr_accessor :value + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @date = attributes[:date] || attributes['date'] + @value = attributes[:value] || attributes['value'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + date: 'date', + value: 'value' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/delete_activity_request_payload.rb b/lib/getstream_ruby/generated/models/delete_activity_request_payload.rb index 610fc58..35ad713 100644 --- a/lib/getstream_ruby/generated/models/delete_activity_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_activity_request_payload.rb @@ -9,6 +9,12 @@ module Models class DeleteActivityRequestPayload < GetStream::BaseModel # Model attributes + # @!attribute entity_id + # @return [String] ID of the activity to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity (required for delete_activity to distinguish v2 vs v3) + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the activity attr_accessor :hard_delete @@ -19,6 +25,8 @@ class DeleteActivityRequestPayload < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @reason = attributes[:reason] || attributes['reason'] || nil end @@ -26,6 +34,8 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', reason: 'reason' } diff --git a/lib/getstream_ruby/generated/models/delete_comment_request_payload.rb b/lib/getstream_ruby/generated/models/delete_comment_request_payload.rb index 09b15e1..b9df98a 100644 --- a/lib/getstream_ruby/generated/models/delete_comment_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_comment_request_payload.rb @@ -9,6 +9,12 @@ module Models class DeleteCommentRequestPayload < GetStream::BaseModel # Model attributes + # @!attribute entity_id + # @return [String] ID of the comment to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the comment attr_accessor :hard_delete @@ -19,6 +25,8 @@ class DeleteCommentRequestPayload < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @reason = attributes[:reason] || attributes['reason'] || nil end @@ -26,6 +34,8 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', reason: 'reason' } diff --git a/lib/getstream_ruby/generated/models/delete_message_request_payload.rb b/lib/getstream_ruby/generated/models/delete_message_request_payload.rb index 14dbfc9..9a027d3 100644 --- a/lib/getstream_ruby/generated/models/delete_message_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_message_request_payload.rb @@ -9,6 +9,12 @@ module Models class DeleteMessageRequestPayload < GetStream::BaseModel # Model attributes + # @!attribute entity_id + # @return [String] ID of the message to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the message attr_accessor :hard_delete @@ -19,6 +25,8 @@ class DeleteMessageRequestPayload < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @reason = attributes[:reason] || attributes['reason'] || nil end @@ -26,6 +34,8 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', reason: 'reason' } diff --git a/lib/getstream_ruby/generated/models/delete_reaction_request_payload.rb b/lib/getstream_ruby/generated/models/delete_reaction_request_payload.rb index afb3968..8ae00ee 100644 --- a/lib/getstream_ruby/generated/models/delete_reaction_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_reaction_request_payload.rb @@ -9,6 +9,12 @@ module Models class DeleteReactionRequestPayload < GetStream::BaseModel # Model attributes + # @!attribute entity_id + # @return [String] ID of the reaction to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the reaction attr_accessor :hard_delete @@ -19,6 +25,8 @@ class DeleteReactionRequestPayload < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @reason = attributes[:reason] || attributes['reason'] || nil end @@ -26,6 +34,8 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', reason: 'reason' } diff --git a/lib/getstream_ruby/generated/models/delete_user_request_payload.rb b/lib/getstream_ruby/generated/models/delete_user_request_payload.rb index 1ba276b..e25dde7 100644 --- a/lib/getstream_ruby/generated/models/delete_user_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_user_request_payload.rb @@ -15,6 +15,12 @@ class DeleteUserRequestPayload < GetStream::BaseModel # @!attribute delete_feeds_content # @return [Boolean] Delete flagged feeds content attr_accessor :delete_feeds_content + # @!attribute entity_id + # @return [String] ID of the user to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the user attr_accessor :hard_delete @@ -30,6 +36,8 @@ def initialize(attributes = {}) super(attributes) @delete_conversation_channels = attributes[:delete_conversation_channels] || attributes['delete_conversation_channels'] || nil @delete_feeds_content = attributes[:delete_feeds_content] || attributes['delete_feeds_content'] || nil + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @mark_messages_deleted = attributes[:mark_messages_deleted] || attributes['mark_messages_deleted'] || nil @reason = attributes[:reason] || attributes['reason'] || nil @@ -40,6 +48,8 @@ def self.json_field_mappings { delete_conversation_channels: 'delete_conversation_channels', delete_feeds_content: 'delete_feeds_content', + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', mark_messages_deleted: 'mark_messages_deleted', reason: 'reason' diff --git a/lib/getstream_ruby/generated/models/feed_group_restored_event.rb b/lib/getstream_ruby/generated/models/feed_group_restored_event.rb new file mode 100644 index 0000000..821064a --- /dev/null +++ b/lib/getstream_ruby/generated/models/feed_group_restored_event.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when a feed group is restored. + class FeedGroupRestoredEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute fid + # @return [String] + attr_accessor :fid + # @!attribute group_id + # @return [String] The ID of the feed group that was restored + attr_accessor :group_id + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "feeds.feed_group.restored" in this case + attr_accessor :type + # @!attribute feed_visibility + # @return [String] + attr_accessor :feed_visibility + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @fid = attributes[:fid] || attributes['fid'] + @group_id = attributes[:group_id] || attributes['group_id'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "feeds.feed_group.restored" + @feed_visibility = attributes[:feed_visibility] || attributes['feed_visibility'] || nil + @received_at = attributes[:received_at] || attributes['received_at'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + fid: 'fid', + group_id: 'group_id', + custom: 'custom', + type: 'type', + feed_visibility: 'feed_visibility', + received_at: 'received_at' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/get_user_group_response.rb b/lib/getstream_ruby/generated/models/get_user_group_response.rb new file mode 100644 index 0000000..a41e20a --- /dev/null +++ b/lib/getstream_ruby/generated/models/get_user_group_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for getting a user group + class GetUserGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/import_v2_task_settings.rb b/lib/getstream_ruby/generated/models/import_v2_task_settings.rb index eccd5f0..30f7f79 100644 --- a/lib/getstream_ruby/generated/models/import_v2_task_settings.rb +++ b/lib/getstream_ruby/generated/models/import_v2_task_settings.rb @@ -9,6 +9,9 @@ module Models class ImportV2TaskSettings < GetStream::BaseModel # Model attributes + # @!attribute merge_custom + # @return [Boolean] + attr_accessor :merge_custom # @!attribute mode # @return [String] attr_accessor :mode @@ -25,6 +28,7 @@ class ImportV2TaskSettings < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @merge_custom = attributes[:merge_custom] || attributes['merge_custom'] || nil @mode = attributes[:mode] || attributes['mode'] || nil @path = attributes[:path] || attributes['path'] || nil @skip_references_check = attributes[:skip_references_check] || attributes['skip_references_check'] || nil @@ -34,6 +38,7 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + merge_custom: 'merge_custom', mode: 'mode', path: 'path', skip_references_check: 'skip_references_check', diff --git a/lib/getstream_ruby/generated/models/individual_record_settings.rb b/lib/getstream_ruby/generated/models/individual_record_settings.rb index 63c0bcf..f5fe2e5 100644 --- a/lib/getstream_ruby/generated/models/individual_record_settings.rb +++ b/lib/getstream_ruby/generated/models/individual_record_settings.rb @@ -12,17 +12,22 @@ class IndividualRecordSettings < GetStream::BaseModel # @!attribute mode # @return [String] attr_accessor :mode + # @!attribute output_types + # @return [Array] + attr_accessor :output_types # Initialize with attributes def initialize(attributes = {}) super(attributes) @mode = attributes[:mode] || attributes['mode'] + @output_types = attributes[:output_types] || attributes['output_types'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { - mode: 'mode' + mode: 'mode', + output_types: 'output_types' } end end diff --git a/lib/getstream_ruby/generated/models/individual_recording_settings_request.rb b/lib/getstream_ruby/generated/models/individual_recording_settings_request.rb index cba368a..1e3c8c4 100644 --- a/lib/getstream_ruby/generated/models/individual_recording_settings_request.rb +++ b/lib/getstream_ruby/generated/models/individual_recording_settings_request.rb @@ -12,17 +12,22 @@ class IndividualRecordingSettingsRequest < GetStream::BaseModel # @!attribute mode # @return [String] Recording mode. One of: available, disabled, auto-on attr_accessor :mode + # @!attribute output_types + # @return [Array] Output types to include: audio_only, video_only, audio_video, screenshare_audio_only, screenshare_video_only, screenshare_audio_video + attr_accessor :output_types # Initialize with attributes def initialize(attributes = {}) super(attributes) @mode = attributes[:mode] || attributes['mode'] + @output_types = attributes[:output_types] || attributes['output_types'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { - mode: 'mode' + mode: 'mode', + output_types: 'output_types' } end end diff --git a/lib/getstream_ruby/generated/models/individual_recording_settings_response.rb b/lib/getstream_ruby/generated/models/individual_recording_settings_response.rb index ab57f2a..f72f920 100644 --- a/lib/getstream_ruby/generated/models/individual_recording_settings_response.rb +++ b/lib/getstream_ruby/generated/models/individual_recording_settings_response.rb @@ -12,17 +12,22 @@ class IndividualRecordingSettingsResponse < GetStream::BaseModel # @!attribute mode # @return [String] attr_accessor :mode + # @!attribute output_types + # @return [Array] + attr_accessor :output_types # Initialize with attributes def initialize(attributes = {}) super(attributes) @mode = attributes[:mode] || attributes['mode'] + @output_types = attributes[:output_types] || attributes['output_types'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { - mode: 'mode' + mode: 'mode', + output_types: 'output_types' } end end diff --git a/lib/getstream_ruby/generated/models/list_user_groups_response.rb b/lib/getstream_ruby/generated/models/list_user_groups_response.rb new file mode 100644 index 0000000..1c79862 --- /dev/null +++ b/lib/getstream_ruby/generated/models/list_user_groups_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for listing user groups + class ListUserGroupsResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_groups + # @return [Array] List of user groups + attr_accessor :user_groups + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_groups = attributes[:user_groups] || attributes['user_groups'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_groups: 'user_groups' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/message_request.rb b/lib/getstream_ruby/generated/models/message_request.rb index f2fb16d..a17fb05 100644 --- a/lib/getstream_ruby/generated/models/message_request.rb +++ b/lib/getstream_ruby/generated/models/message_request.rb @@ -60,6 +60,9 @@ class MessageRequest < GetStream::BaseModel # @!attribute attachments # @return [Array] Array of message attachments attr_accessor :attachments + # @!attribute mentioned_roles + # @return [Array] + attr_accessor :mentioned_roles # @!attribute mentioned_users # @return [Array] Array of user IDs to mention attr_accessor :mentioned_users @@ -96,6 +99,7 @@ def initialize(attributes = {}) @type = attributes[:type] || attributes['type'] || nil @user_id = attributes[:user_id] || attributes['user_id'] || nil @attachments = attributes[:attachments] || attributes['attachments'] || nil + @mentioned_roles = attributes[:mentioned_roles] || attributes['mentioned_roles'] || nil @mentioned_users = attributes[:mentioned_users] || attributes['mentioned_users'] || nil @restricted_visibility = attributes[:restricted_visibility] || attributes['restricted_visibility'] || nil @custom = attributes[:custom] || attributes['custom'] || nil @@ -123,6 +127,7 @@ def self.json_field_mappings type: 'type', user_id: 'user_id', attachments: 'attachments', + mentioned_roles: 'mentioned_roles', mentioned_users: 'mentioned_users', restricted_visibility: 'restricted_visibility', custom: 'custom', diff --git a/lib/getstream_ruby/generated/models/message_response.rb b/lib/getstream_ruby/generated/models/message_response.rb index a49ba4d..fd34cb4 100644 --- a/lib/getstream_ruby/generated/models/message_response.rb +++ b/lib/getstream_ruby/generated/models/message_response.rb @@ -111,6 +111,9 @@ class MessageResponse < GetStream::BaseModel # @!attribute show_in_channel # @return [Boolean] Whether thread reply should be shown in the channel as well attr_accessor :show_in_channel + # @!attribute mentioned_roles + # @return [Array] List of roles mentioned in the message (e.g. admin, channel_moderator, custom roles). Members with matching roles will receive push notifications based on their push preferences. Max 10 roles + attr_accessor :mentioned_roles # @!attribute thread_participants # @return [Array] List of users who participate in thread attr_accessor :thread_participants @@ -185,6 +188,7 @@ def initialize(attributes = {}) @poll_id = attributes[:poll_id] || attributes['poll_id'] || nil @quoted_message_id = attributes[:quoted_message_id] || attributes['quoted_message_id'] || nil @show_in_channel = attributes[:show_in_channel] || attributes['show_in_channel'] || nil + @mentioned_roles = attributes[:mentioned_roles] || attributes['mentioned_roles'] || nil @thread_participants = attributes[:thread_participants] || attributes['thread_participants'] || nil @draft = attributes[:draft] || attributes['draft'] || nil @i18n = attributes[:i18n] || attributes['i18n'] || nil @@ -236,6 +240,7 @@ def self.json_field_mappings poll_id: 'poll_id', quoted_message_id: 'quoted_message_id', show_in_channel: 'show_in_channel', + mentioned_roles: 'mentioned_roles', thread_participants: 'thread_participants', draft: 'draft', i18n: 'i18n', diff --git a/lib/getstream_ruby/generated/models/message_with_channel_response.rb b/lib/getstream_ruby/generated/models/message_with_channel_response.rb index 99934f0..5400b70 100644 --- a/lib/getstream_ruby/generated/models/message_with_channel_response.rb +++ b/lib/getstream_ruby/generated/models/message_with_channel_response.rb @@ -114,6 +114,9 @@ class MessageWithChannelResponse < GetStream::BaseModel # @!attribute show_in_channel # @return [Boolean] Whether thread reply should be shown in the channel as well attr_accessor :show_in_channel + # @!attribute mentioned_roles + # @return [Array] List of roles mentioned in the message (e.g. admin, channel_moderator, custom roles). Members with matching roles will receive push notifications based on their push preferences. Max 10 roles + attr_accessor :mentioned_roles # @!attribute thread_participants # @return [Array] List of users who participate in thread attr_accessor :thread_participants @@ -189,6 +192,7 @@ def initialize(attributes = {}) @poll_id = attributes[:poll_id] || attributes['poll_id'] || nil @quoted_message_id = attributes[:quoted_message_id] || attributes['quoted_message_id'] || nil @show_in_channel = attributes[:show_in_channel] || attributes['show_in_channel'] || nil + @mentioned_roles = attributes[:mentioned_roles] || attributes['mentioned_roles'] || nil @thread_participants = attributes[:thread_participants] || attributes['thread_participants'] || nil @draft = attributes[:draft] || attributes['draft'] || nil @i18n = attributes[:i18n] || attributes['i18n'] || nil @@ -241,6 +245,7 @@ def self.json_field_mappings poll_id: 'poll_id', quoted_message_id: 'quoted_message_id', show_in_channel: 'show_in_channel', + mentioned_roles: 'mentioned_roles', thread_participants: 'thread_participants', draft: 'draft', i18n: 'i18n', diff --git a/lib/getstream_ruby/generated/models/metric_stats.rb b/lib/getstream_ruby/generated/models/metric_stats.rb new file mode 100644 index 0000000..ef557c3 --- /dev/null +++ b/lib/getstream_ruby/generated/models/metric_stats.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Statistics for a single metric with optional daily breakdown + class MetricStats < GetStream::BaseModel + + # Model attributes + # @!attribute total + # @return [Integer] Aggregated total value + attr_accessor :total + # @!attribute daily + # @return [Array] Per-day values (only present in daily mode) + attr_accessor :daily + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @total = attributes[:total] || attributes['total'] + @daily = attributes[:daily] || attributes['daily'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + total: 'total', + daily: 'daily' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/query_team_usage_stats_request.rb b/lib/getstream_ruby/generated/models/query_team_usage_stats_request.rb new file mode 100644 index 0000000..1a008c0 --- /dev/null +++ b/lib/getstream_ruby/generated/models/query_team_usage_stats_request.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request payload for querying team-level usage statistics from the warehouse database + class QueryTeamUsageStatsRequest < GetStream::BaseModel + + # Model attributes + # @!attribute end_date + # @return [String] End date in YYYY-MM-DD format. Used with start_date for custom date range. Returns daily breakdown. + attr_accessor :end_date + # @!attribute limit + # @return [Integer] Maximum number of teams to return per page (default: 30, max: 30) + attr_accessor :limit + # @!attribute month + # @return [String] Month in YYYY-MM format (e.g., '2026-01'). Mutually exclusive with start_date/end_date. Returns aggregated monthly values. + attr_accessor :month + # @!attribute next + # @return [String] Cursor for pagination to fetch next page of teams + attr_accessor :next + # @!attribute start_date + # @return [String] Start date in YYYY-MM-DD format. Used with end_date for custom date range. Returns daily breakdown. + attr_accessor :start_date + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @end_date = attributes[:end_date] || attributes['end_date'] || nil + @limit = attributes[:limit] || attributes['limit'] || nil + @month = attributes[:month] || attributes['month'] || nil + @next = attributes[:next] || attributes['next'] || nil + @start_date = attributes[:start_date] || attributes['start_date'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + end_date: 'end_date', + limit: 'limit', + month: 'month', + next: 'next', + start_date: 'start_date' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/query_team_usage_stats_response.rb b/lib/getstream_ruby/generated/models/query_team_usage_stats_response.rb new file mode 100644 index 0000000..377cbec --- /dev/null +++ b/lib/getstream_ruby/generated/models/query_team_usage_stats_response.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response containing team-level usage statistics + class QueryTeamUsageStatsResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] Duration of the request in milliseconds + attr_accessor :duration + # @!attribute teams + # @return [Array] Array of team usage statistics + attr_accessor :teams + # @!attribute next + # @return [String] Cursor for pagination to fetch next page + attr_accessor :next + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @teams = attributes[:teams] || attributes['teams'] + @next = attributes[:next] || attributes['next'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + teams: 'teams', + next: 'next' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/read_collections_response.rb b/lib/getstream_ruby/generated/models/read_collections_response.rb index 2845c58..89f1b12 100644 --- a/lib/getstream_ruby/generated/models/read_collections_response.rb +++ b/lib/getstream_ruby/generated/models/read_collections_response.rb @@ -15,19 +15,29 @@ class ReadCollectionsResponse < GetStream::BaseModel # @!attribute collections # @return [Array] List of collections matching the query attr_accessor :collections + # @!attribute next + # @return [String] Cursor for next page (when listing without collection_refs) + attr_accessor :next + # @!attribute prev + # @return [String] Cursor for previous page (when listing without collection_refs) + attr_accessor :prev # Initialize with attributes def initialize(attributes = {}) super(attributes) @duration = attributes[:duration] || attributes['duration'] @collections = attributes[:collections] || attributes['collections'] + @next = attributes[:next] || attributes['next'] || nil + @prev = attributes[:prev] || attributes['prev'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { duration: 'duration', - collections: 'collections' + collections: 'collections', + next: 'next', + prev: 'prev' } end end diff --git a/lib/getstream_ruby/generated/models/remove_user_group_members_response.rb b/lib/getstream_ruby/generated/models/remove_user_group_members_response.rb new file mode 100644 index 0000000..c78add5 --- /dev/null +++ b/lib/getstream_ruby/generated/models/remove_user_group_members_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for removing members from a user group + class RemoveUserGroupMembersResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/restore_feed_group_request.rb b/lib/getstream_ruby/generated/models/restore_feed_group_request.rb new file mode 100644 index 0000000..55581de --- /dev/null +++ b/lib/getstream_ruby/generated/models/restore_feed_group_request.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class RestoreFeedGroupRequest < GetStream::BaseModel + # Empty model - inherits all functionality from BaseModel + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/restore_feed_group_response.rb b/lib/getstream_ruby/generated/models/restore_feed_group_response.rb new file mode 100644 index 0000000..0d82a0e --- /dev/null +++ b/lib/getstream_ruby/generated/models/restore_feed_group_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class RestoreFeedGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute feed_group + # @return [FeedGroupResponse] + attr_accessor :feed_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @feed_group = attributes[:feed_group] || attributes['feed_group'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + feed_group: 'feed_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/rule_builder_condition.rb b/lib/getstream_ruby/generated/models/rule_builder_condition.rb index f7b5ed2..16d4739 100644 --- a/lib/getstream_ruby/generated/models/rule_builder_condition.rb +++ b/lib/getstream_ruby/generated/models/rule_builder_condition.rb @@ -21,6 +21,9 @@ class RuleBuilderCondition < GetStream::BaseModel # @!attribute call_type_rule_params # @return [CallTypeRuleParameters] attr_accessor :call_type_rule_params + # @!attribute call_violation_count_params + # @return [CallViolationCountParameters] + attr_accessor :call_violation_count_params # @!attribute closed_caption_rule_params # @return [ClosedCaptionRuleParameters] attr_accessor :closed_caption_rule_params @@ -77,6 +80,7 @@ def initialize(attributes = {}) @type = attributes[:type] || attributes['type'] || nil @call_custom_property_params = attributes[:call_custom_property_params] || attributes['call_custom_property_params'] || nil @call_type_rule_params = attributes[:call_type_rule_params] || attributes['call_type_rule_params'] || nil + @call_violation_count_params = attributes[:call_violation_count_params] || attributes['call_violation_count_params'] || nil @closed_caption_rule_params = attributes[:closed_caption_rule_params] || attributes['closed_caption_rule_params'] || nil @content_count_rule_params = attributes[:content_count_rule_params] || attributes['content_count_rule_params'] || nil @content_flag_count_rule_params = attributes[:content_flag_count_rule_params] || attributes['content_flag_count_rule_params'] || nil @@ -102,6 +106,7 @@ def self.json_field_mappings type: 'type', call_custom_property_params: 'call_custom_property_params', call_type_rule_params: 'call_type_rule_params', + call_violation_count_params: 'call_violation_count_params', closed_caption_rule_params: 'closed_caption_rule_params', content_count_rule_params: 'content_count_rule_params', content_flag_count_rule_params: 'content_flag_count_rule_params', diff --git a/lib/getstream_ruby/generated/models/search_result_message.rb b/lib/getstream_ruby/generated/models/search_result_message.rb index 348034f..c54004c 100644 --- a/lib/getstream_ruby/generated/models/search_result_message.rb +++ b/lib/getstream_ruby/generated/models/search_result_message.rb @@ -111,6 +111,9 @@ class SearchResultMessage < GetStream::BaseModel # @!attribute show_in_channel # @return [Boolean] attr_accessor :show_in_channel + # @!attribute mentioned_roles + # @return [Array] + attr_accessor :mentioned_roles # @!attribute thread_participants # @return [Array] attr_accessor :thread_participants @@ -188,6 +191,7 @@ def initialize(attributes = {}) @poll_id = attributes[:poll_id] || attributes['poll_id'] || nil @quoted_message_id = attributes[:quoted_message_id] || attributes['quoted_message_id'] || nil @show_in_channel = attributes[:show_in_channel] || attributes['show_in_channel'] || nil + @mentioned_roles = attributes[:mentioned_roles] || attributes['mentioned_roles'] || nil @thread_participants = attributes[:thread_participants] || attributes['thread_participants'] || nil @channel = attributes[:channel] || attributes['channel'] || nil @draft = attributes[:draft] || attributes['draft'] || nil @@ -240,6 +244,7 @@ def self.json_field_mappings poll_id: 'poll_id', quoted_message_id: 'quoted_message_id', show_in_channel: 'show_in_channel', + mentioned_roles: 'mentioned_roles', thread_participants: 'thread_participants', channel: 'channel', draft: 'draft', diff --git a/lib/getstream_ruby/generated/models/search_user_groups_response.rb b/lib/getstream_ruby/generated/models/search_user_groups_response.rb new file mode 100644 index 0000000..424a14d --- /dev/null +++ b/lib/getstream_ruby/generated/models/search_user_groups_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for searching user groups + class SearchUserGroupsResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_groups + # @return [Array] List of matching user groups + attr_accessor :user_groups + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_groups = attributes[:user_groups] || attributes['user_groups'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_groups: 'user_groups' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/team_usage_stats.rb b/lib/getstream_ruby/generated/models/team_usage_stats.rb new file mode 100644 index 0000000..0b8b25b --- /dev/null +++ b/lib/getstream_ruby/generated/models/team_usage_stats.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Usage statistics for a single team containing all 16 metrics + class TeamUsageStats < GetStream::BaseModel + + # Model attributes + # @!attribute team + # @return [String] Team identifier (empty string for users not assigned to any team) + attr_accessor :team + # @!attribute concurrent_connections + # @return [MetricStats] + attr_accessor :concurrent_connections + # @!attribute concurrent_users + # @return [MetricStats] + attr_accessor :concurrent_users + # @!attribute image_moderations_daily + # @return [MetricStats] + attr_accessor :image_moderations_daily + # @!attribute messages_daily + # @return [MetricStats] + attr_accessor :messages_daily + # @!attribute messages_last_24_hours + # @return [MetricStats] + attr_accessor :messages_last_24_hours + # @!attribute messages_last_30_days + # @return [MetricStats] + attr_accessor :messages_last_30_days + # @!attribute messages_month_to_date + # @return [MetricStats] + attr_accessor :messages_month_to_date + # @!attribute messages_total + # @return [MetricStats] + attr_accessor :messages_total + # @!attribute translations_daily + # @return [MetricStats] + attr_accessor :translations_daily + # @!attribute users_daily + # @return [MetricStats] + attr_accessor :users_daily + # @!attribute users_engaged_last_30_days + # @return [MetricStats] + attr_accessor :users_engaged_last_30_days + # @!attribute users_engaged_month_to_date + # @return [MetricStats] + attr_accessor :users_engaged_month_to_date + # @!attribute users_last_24_hours + # @return [MetricStats] + attr_accessor :users_last_24_hours + # @!attribute users_last_30_days + # @return [MetricStats] + attr_accessor :users_last_30_days + # @!attribute users_month_to_date + # @return [MetricStats] + attr_accessor :users_month_to_date + # @!attribute users_total + # @return [MetricStats] + attr_accessor :users_total + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @team = attributes[:team] || attributes['team'] + @concurrent_connections = attributes[:concurrent_connections] || attributes['concurrent_connections'] + @concurrent_users = attributes[:concurrent_users] || attributes['concurrent_users'] + @image_moderations_daily = attributes[:image_moderations_daily] || attributes['image_moderations_daily'] + @messages_daily = attributes[:messages_daily] || attributes['messages_daily'] + @messages_last_24_hours = attributes[:messages_last_24_hours] || attributes['messages_last_24_hours'] + @messages_last_30_days = attributes[:messages_last_30_days] || attributes['messages_last_30_days'] + @messages_month_to_date = attributes[:messages_month_to_date] || attributes['messages_month_to_date'] + @messages_total = attributes[:messages_total] || attributes['messages_total'] + @translations_daily = attributes[:translations_daily] || attributes['translations_daily'] + @users_daily = attributes[:users_daily] || attributes['users_daily'] + @users_engaged_last_30_days = attributes[:users_engaged_last_30_days] || attributes['users_engaged_last_30_days'] + @users_engaged_month_to_date = attributes[:users_engaged_month_to_date] || attributes['users_engaged_month_to_date'] + @users_last_24_hours = attributes[:users_last_24_hours] || attributes['users_last_24_hours'] + @users_last_30_days = attributes[:users_last_30_days] || attributes['users_last_30_days'] + @users_month_to_date = attributes[:users_month_to_date] || attributes['users_month_to_date'] + @users_total = attributes[:users_total] || attributes['users_total'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + team: 'team', + concurrent_connections: 'concurrent_connections', + concurrent_users: 'concurrent_users', + image_moderations_daily: 'image_moderations_daily', + messages_daily: 'messages_daily', + messages_last_24_hours: 'messages_last_24_hours', + messages_last_30_days: 'messages_last_30_days', + messages_month_to_date: 'messages_month_to_date', + messages_total: 'messages_total', + translations_daily: 'translations_daily', + users_daily: 'users_daily', + users_engaged_last_30_days: 'users_engaged_last_30_days', + users_engaged_month_to_date: 'users_engaged_month_to_date', + users_last_24_hours: 'users_last_24_hours', + users_last_30_days: 'users_last_30_days', + users_month_to_date: 'users_month_to_date', + users_total: 'users_total' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/undelete_message_request.rb b/lib/getstream_ruby/generated/models/undelete_message_request.rb new file mode 100644 index 0000000..bf746b1 --- /dev/null +++ b/lib/getstream_ruby/generated/models/undelete_message_request.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class UndeleteMessageRequest < GetStream::BaseModel + + # Model attributes + # @!attribute undeleted_by + # @return [String] ID of the user who is undeleting the message + attr_accessor :undeleted_by + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @undeleted_by = attributes[:undeleted_by] || attributes['undeleted_by'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + undeleted_by: 'undeleted_by' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/undelete_message_response.rb b/lib/getstream_ruby/generated/models/undelete_message_response.rb new file mode 100644 index 0000000..ab348cb --- /dev/null +++ b/lib/getstream_ruby/generated/models/undelete_message_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Basic response information + class UndeleteMessageResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] Duration of the request in milliseconds + attr_accessor :duration + # @!attribute message + # @return [MessageResponse] + attr_accessor :message + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @message = attributes[:message] || attributes['message'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + message: 'message' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/update_app_request.rb b/lib/getstream_ruby/generated/models/update_app_request.rb index 6644078..fce54e5 100644 --- a/lib/getstream_ruby/generated/models/update_app_request.rb +++ b/lib/getstream_ruby/generated/models/update_app_request.rb @@ -54,6 +54,9 @@ class UpdateAppRequest < GetStream::BaseModel # @!attribute migrate_permissions_to_v2 # @return [Boolean] attr_accessor :migrate_permissions_to_v2 + # @!attribute moderation_analytics_enabled + # @return [Boolean] + attr_accessor :moderation_analytics_enabled # @!attribute moderation_enabled # @return [Boolean] attr_accessor :moderation_enabled @@ -169,6 +172,7 @@ def initialize(attributes = {}) @image_moderation_enabled = attributes[:image_moderation_enabled] || attributes['image_moderation_enabled'] || nil @max_aggregated_activities_length = attributes[:max_aggregated_activities_length] || attributes['max_aggregated_activities_length'] || nil @migrate_permissions_to_v2 = attributes[:migrate_permissions_to_v2] || attributes['migrate_permissions_to_v2'] || nil + @moderation_analytics_enabled = attributes[:moderation_analytics_enabled] || attributes['moderation_analytics_enabled'] || nil @moderation_enabled = attributes[:moderation_enabled] || attributes['moderation_enabled'] || nil @moderation_webhook_url = attributes[:moderation_webhook_url] || attributes['moderation_webhook_url'] || nil @multi_tenant_enabled = attributes[:multi_tenant_enabled] || attributes['multi_tenant_enabled'] || nil @@ -221,6 +225,7 @@ def self.json_field_mappings image_moderation_enabled: 'image_moderation_enabled', max_aggregated_activities_length: 'max_aggregated_activities_length', migrate_permissions_to_v2: 'migrate_permissions_to_v2', + moderation_analytics_enabled: 'moderation_analytics_enabled', moderation_enabled: 'moderation_enabled', moderation_webhook_url: 'moderation_webhook_url', multi_tenant_enabled: 'multi_tenant_enabled', diff --git a/lib/getstream_ruby/generated/models/update_user_group_request.rb b/lib/getstream_ruby/generated/models/update_user_group_request.rb new file mode 100644 index 0000000..793c3e0 --- /dev/null +++ b/lib/getstream_ruby/generated/models/update_user_group_request.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request body for updating a user group + class UpdateUserGroupRequest < GetStream::BaseModel + + # Model attributes + # @!attribute description + # @return [String] The new description for the group + attr_accessor :description + # @!attribute name + # @return [String] The new name of the user group + attr_accessor :name + # @!attribute team_id + # @return [String] + attr_accessor :team_id + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @description = attributes[:description] || attributes['description'] || nil + @name = attributes[:name] || attributes['name'] || nil + @team_id = attributes[:team_id] || attributes['team_id'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + description: 'description', + name: 'name', + team_id: 'team_id' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/update_user_group_response.rb b/lib/getstream_ruby/generated/models/update_user_group_response.rb new file mode 100644 index 0000000..9e98bbf --- /dev/null +++ b/lib/getstream_ruby/generated/models/update_user_group_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for updating a user group + class UpdateUserGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group.rb b/lib/getstream_ruby/generated/models/user_group.rb new file mode 100644 index 0000000..6328ecd --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class UserGroup < GetStream::BaseModel + + # Model attributes + # @!attribute app_pk + # @return [Integer] + attr_accessor :app_pk + # @!attribute created_at + # @return [DateTime] + attr_accessor :created_at + # @!attribute id + # @return [String] + attr_accessor :id + # @!attribute name + # @return [String] + attr_accessor :name + # @!attribute updated_at + # @return [DateTime] + attr_accessor :updated_at + # @!attribute created_by + # @return [String] + attr_accessor :created_by + # @!attribute description + # @return [String] + attr_accessor :description + # @!attribute team_id + # @return [String] + attr_accessor :team_id + # @!attribute members + # @return [Array] + attr_accessor :members + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @app_pk = attributes[:app_pk] || attributes['app_pk'] + @created_at = attributes[:created_at] || attributes['created_at'] + @id = attributes[:id] || attributes['id'] + @name = attributes[:name] || attributes['name'] + @updated_at = attributes[:updated_at] || attributes['updated_at'] + @created_by = attributes[:created_by] || attributes['created_by'] || nil + @description = attributes[:description] || attributes['description'] || nil + @team_id = attributes[:team_id] || attributes['team_id'] || nil + @members = attributes[:members] || attributes['members'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + app_pk: 'app_pk', + created_at: 'created_at', + id: 'id', + name: 'name', + updated_at: 'updated_at', + created_by: 'created_by', + description: 'description', + team_id: 'team_id', + members: 'members' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_created_event.rb b/lib/getstream_ruby/generated/models/user_group_created_event.rb new file mode 100644 index 0000000..cbe7234 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_created_event.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when a user group is created. + class UserGroupCreatedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.created" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.created" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_deleted_event.rb b/lib/getstream_ruby/generated/models/user_group_deleted_event.rb new file mode 100644 index 0000000..b09fc27 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_deleted_event.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when a user group is deleted. + class UserGroupDeletedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.deleted" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.deleted" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_member.rb b/lib/getstream_ruby/generated/models/user_group_member.rb new file mode 100644 index 0000000..75aef41 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_member.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class UserGroupMember < GetStream::BaseModel + + # Model attributes + # @!attribute app_pk + # @return [Integer] + attr_accessor :app_pk + # @!attribute created_at + # @return [DateTime] + attr_accessor :created_at + # @!attribute group_id + # @return [String] + attr_accessor :group_id + # @!attribute is_admin + # @return [Boolean] + attr_accessor :is_admin + # @!attribute user_id + # @return [String] + attr_accessor :user_id + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @app_pk = attributes[:app_pk] || attributes['app_pk'] + @created_at = attributes[:created_at] || attributes['created_at'] + @group_id = attributes[:group_id] || attributes['group_id'] + @is_admin = attributes[:is_admin] || attributes['is_admin'] + @user_id = attributes[:user_id] || attributes['user_id'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + app_pk: 'app_pk', + created_at: 'created_at', + group_id: 'group_id', + is_admin: 'is_admin', + user_id: 'user_id' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_member_added_event.rb b/lib/getstream_ruby/generated/models/user_group_member_added_event.rb new file mode 100644 index 0000000..7efeccd --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_member_added_event.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when members are added to a user group. + class UserGroupMemberAddedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute members + # @return [Array] The user IDs that were added + attr_accessor :members + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.member_added" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @members = attributes[:members] || attributes['members'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.member_added" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + members: 'members', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_member_removed_event.rb b/lib/getstream_ruby/generated/models/user_group_member_removed_event.rb new file mode 100644 index 0000000..c6edf41 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_member_removed_event.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when members are removed from a user group. + class UserGroupMemberRemovedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute members + # @return [Array] The user IDs that were removed + attr_accessor :members + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.member_removed" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @members = attributes[:members] || attributes['members'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.member_removed" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + members: 'members', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_response.rb b/lib/getstream_ruby/generated/models/user_group_response.rb new file mode 100644 index 0000000..2d7d8c0 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_response.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class UserGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] + attr_accessor :created_at + # @!attribute id + # @return [String] + attr_accessor :id + # @!attribute name + # @return [String] + attr_accessor :name + # @!attribute updated_at + # @return [DateTime] + attr_accessor :updated_at + # @!attribute created_by + # @return [String] + attr_accessor :created_by + # @!attribute description + # @return [String] + attr_accessor :description + # @!attribute team_id + # @return [String] + attr_accessor :team_id + # @!attribute members + # @return [Array] + attr_accessor :members + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @id = attributes[:id] || attributes['id'] + @name = attributes[:name] || attributes['name'] + @updated_at = attributes[:updated_at] || attributes['updated_at'] + @created_by = attributes[:created_by] || attributes['created_by'] || nil + @description = attributes[:description] || attributes['description'] || nil + @team_id = attributes[:team_id] || attributes['team_id'] || nil + @members = attributes[:members] || attributes['members'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + id: 'id', + name: 'name', + updated_at: 'updated_at', + created_by: 'created_by', + description: 'description', + team_id: 'team_id', + members: 'members' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_updated_event.rb b/lib/getstream_ruby/generated/models/user_group_updated_event.rb new file mode 100644 index 0000000..3ebb5c2 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_updated_event.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when a user group is updated. + class UserGroupUpdatedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.updated" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.updated" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/webhook.rb b/lib/getstream_ruby/generated/webhook.rb index 017a64e..266f1f4 100644 --- a/lib/getstream_ruby/generated/webhook.rb +++ b/lib/getstream_ruby/generated/webhook.rb @@ -102,6 +102,7 @@ require_relative 'models/feed_deleted_event' require_relative 'models/feed_group_changed_event' require_relative 'models/feed_group_deleted_event' +require_relative 'models/feed_group_restored_event' require_relative 'models/feed_member_added_event' require_relative 'models/feed_member_removed_event' require_relative 'models/feed_member_updated_event' @@ -152,6 +153,11 @@ require_relative 'models/user_deactivated_event' require_relative 'models/user_deleted_event' require_relative 'models/user_flagged_event' +require_relative 'models/user_group_created_event' +require_relative 'models/user_group_deleted_event' +require_relative 'models/user_group_member_added_event' +require_relative 'models/user_group_member_removed_event' +require_relative 'models/user_group_updated_event' require_relative 'models/user_messages_deleted_event' require_relative 'models/user_muted_event' require_relative 'models/user_reactivated_event' @@ -272,6 +278,7 @@ module Webhook EVENT_TYPE_FEEDS_FEED_UPDATED = 'feeds.feed.updated' EVENT_TYPE_FEEDS_FEED_GROUP_CHANGED = 'feeds.feed_group.changed' EVENT_TYPE_FEEDS_FEED_GROUP_DELETED = 'feeds.feed_group.deleted' + EVENT_TYPE_FEEDS_FEED_GROUP_RESTORED = 'feeds.feed_group.restored' EVENT_TYPE_FEEDS_FEED_MEMBER_ADDED = 'feeds.feed_member.added' EVENT_TYPE_FEEDS_FEED_MEMBER_REMOVED = 'feeds.feed_member.removed' EVENT_TYPE_FEEDS_FEED_MEMBER_UPDATED = 'feeds.feed_member.updated' @@ -323,6 +330,11 @@ module Webhook EVENT_TYPE_USER_UNMUTED = 'user.unmuted' EVENT_TYPE_USER_UNREAD_MESSAGE_REMINDER = 'user.unread_message_reminder' EVENT_TYPE_USER_UPDATED = 'user.updated' + EVENT_TYPE_USER_GROUP_CREATED = 'user_group.created' + EVENT_TYPE_USER_GROUP_DELETED = 'user_group.deleted' + EVENT_TYPE_USER_GROUP_MEMBER_ADDED = 'user_group.member_added' + EVENT_TYPE_USER_GROUP_MEMBER_REMOVED = 'user_group.member_removed' + EVENT_TYPE_USER_GROUP_UPDATED = 'user_group.updated' # Extract the event type from a raw webhook payload. # @@ -586,6 +598,8 @@ def self.parse_webhook_event(raw_event) StreamChat::FeedGroupChangedEvent when 'feeds.feed_group.deleted' StreamChat::FeedGroupDeletedEvent + when 'feeds.feed_group.restored' + StreamChat::FeedGroupRestoredEvent when 'feeds.feed_member.added' StreamChat::FeedMemberAddedEvent when 'feeds.feed_member.removed' @@ -688,6 +702,16 @@ def self.parse_webhook_event(raw_event) StreamChat::UserUnreadReminderEvent when 'user.updated' StreamChat::UserUpdatedEvent + when 'user_group.created' + StreamChat::UserGroupCreatedEvent + when 'user_group.deleted' + StreamChat::UserGroupDeletedEvent + when 'user_group.member_added' + StreamChat::UserGroupMemberAddedEvent + when 'user_group.member_removed' + StreamChat::UserGroupMemberRemovedEvent + when 'user_group.updated' + StreamChat::UserGroupUpdatedEvent else nil end diff --git a/test/webhook_test.rb b/test/webhook_test.rb index 229dc65..b0424fc 100644 --- a/test/webhook_test.rb +++ b/test/webhook_test.rb @@ -596,6 +596,11 @@ def test_parse_feeds_feed_group_deleted assert_equal 'StreamChat::FeedGroupDeletedEvent', event.class.name end + def test_parse_feeds_feed_group_restored + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_group.restored"}') + assert_equal 'StreamChat::FeedGroupRestoredEvent', event.class.name + end + def test_parse_feeds_feed_member_added event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_member.added"}') assert_equal 'StreamChat::FeedMemberAddedEvent', event.class.name @@ -851,6 +856,31 @@ def test_parse_user_updated assert_equal 'StreamChat::UserUpdatedEvent', event.class.name end + def test_parse_user_group_created + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.created"}') + assert_equal 'StreamChat::UserGroupCreatedEvent', event.class.name + end + + def test_parse_user_group_deleted + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.deleted"}') + assert_equal 'StreamChat::UserGroupDeletedEvent', event.class.name + end + + def test_parse_user_group_member_added + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.member_added"}') + assert_equal 'StreamChat::UserGroupMemberAddedEvent', event.class.name + end + + def test_parse_user_group_member_removed + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.member_removed"}') + assert_equal 'StreamChat::UserGroupMemberRemovedEvent', event.class.name + end + + def test_parse_user_group_updated + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.updated"}') + assert_equal 'StreamChat::UserGroupUpdatedEvent', event.class.name + end + def test_parse_webhook_event_unknown_type assert_raises(ArgumentError) do StreamChat::Webhook.parse_webhook_event('{"type":"unknown.event"}') From 529986583d7913f02e29ca80b247a169352320c6 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 11:07:01 +0100 Subject: [PATCH 22/29] test: add test for user group --- lib/getstream_ruby/generated/common_client.rb | 8 +- .../remove_user_group_members_request.rb | 36 +++ .../chat_user_group_integration_spec.rb | 289 ++++++++++++++++++ 3 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 lib/getstream_ruby/generated/models/remove_user_group_members_request.rb create mode 100644 spec/integration/chat_user_group_integration_spec.rb diff --git a/lib/getstream_ruby/generated/common_client.rb b/lib/getstream_ruby/generated/common_client.rb index 7e4d53e..07ee74b 100644 --- a/lib/getstream_ruby/generated/common_client.rb +++ b/lib/getstream_ruby/generated/common_client.rb @@ -1159,16 +1159,20 @@ def update_user_group(_id, update_user_group_request) # Removes members from a user group. Users already not in the group are silently ignored. # # @param _id [String] + # @param remove_user_group_members_request [RemoveUserGroupMembersRequest] # @return [Models::RemoveUserGroupMembersResponse] - def remove_user_group_members(_id) + def remove_user_group_members(_id, remove_user_group_members_request) path = '/api/v2/usergroups/{id}/members' # Replace path parameters path = path.gsub('{id}', _id.to_s) + # Build request body + body = remove_user_group_members_request # Make the API request @client.make_request( :delete, - path + path, + body: body ) end diff --git a/lib/getstream_ruby/generated/models/remove_user_group_members_request.rb b/lib/getstream_ruby/generated/models/remove_user_group_members_request.rb new file mode 100644 index 0000000..85be53d --- /dev/null +++ b/lib/getstream_ruby/generated/models/remove_user_group_members_request.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request body for removing members from a user group + class RemoveUserGroupMembersRequest < GetStream::BaseModel + + # Model attributes + # @!attribute member_ids + # @return [Array] List of user IDs to remove from the group + attr_accessor :member_ids + # @!attribute team_id + # @return [String] + attr_accessor :team_id + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @member_ids = attributes[:member_ids] || attributes['member_ids'] + @team_id = attributes[:team_id] || attributes['team_id'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + member_ids: 'member_ids', + team_id: 'team_id' + } + end + end + end + end +end diff --git a/spec/integration/chat_user_group_integration_spec.rb b/spec/integration/chat_user_group_integration_spec.rb new file mode 100644 index 0000000..5cd3e47 --- /dev/null +++ b/spec/integration/chat_user_group_integration_spec.rb @@ -0,0 +1,289 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat User Group Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + @created_group_ids = [] + + end + + after(:all) do + + @created_group_ids&.each do |gid| + + @client.common.delete_user_group(gid) + rescue StandardError => e + puts "Warning: Failed to delete user group #{gid}: #{e.message}" + + end + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Helper: create a group and track it for cleanup + # --------------------------------------------------------------------------- + + def create_group(id:, name:, description: nil, member_ids: nil) + req = GetStream::Generated::Models::CreateUserGroupRequest.new( + id: id, + name: name, + description: description, + member_ids: member_ids, + ) + resp = @client.common.create_user_group(req) + @created_group_ids << id + resp + rescue GetStreamRuby::APIError => e + skip 'User groups feature not available for this app' if e.message.include?('Not Found') + raise + end + + def delete_group(id) + @client.common.delete_user_group(id) + @created_group_ids.delete(id) + rescue StandardError + @created_group_ids.delete(id) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'CreateAndGetUserGroup' do + + it 'creates a group with name and description, then retrieves it by ID' do + + group_id = "test-group-#{SecureRandom.uuid}" + group_name = "Test Group #{group_id[0..14]}" + description = 'A test user group' + + create_resp = create_group(id: group_id, name: group_name, description: description) + expect(create_resp.user_group).not_to be_nil + expect(create_resp.user_group.id).to eq(group_id) + expect(create_resp.user_group.name).to eq(group_name) + expect(create_resp.user_group.description).to eq(description) + + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group).not_to be_nil + expect(get_resp.user_group.id).to eq(group_id) + expect(get_resp.user_group.name).to eq(group_name) + + end + + end + + describe 'CreateUserGroupWithInitialMembers' do + + it 'creates a group with initial member IDs and verifies members are present' do + + user_ids, _resp = create_test_users(2) + group_id = "test-group-#{SecureRandom.uuid}" + + create_resp = create_group(id: group_id, name: 'Group With Members', member_ids: user_ids) + expect(create_resp.user_group).not_to be_nil + expect(create_resp.user_group.id).to eq(group_id) + + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group).not_to be_nil + + members = get_resp.user_group.members || [] + found_ids = members.map { |m| m.is_a?(Hash) ? m['user_id'] : m.user_id } + user_ids.each do |uid| + + expect(found_ids).to include(uid) + + end + + end + + end + + describe 'UpdateUserGroup' do + + it 'updates the group name and description, then confirms via GET' do + + group_id = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id, name: 'Original Name') + + new_name = 'Updated Name' + new_desc = 'Updated description' + update_resp = @client.common.update_user_group( + group_id, + GetStream::Generated::Models::UpdateUserGroupRequest.new( + name: new_name, + description: new_desc, + ), + ) + expect(update_resp.user_group).not_to be_nil + expect(update_resp.user_group.name).to eq(new_name) + expect(update_resp.user_group.description).to eq(new_desc) + + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group).not_to be_nil + expect(get_resp.user_group.name).to eq(new_name) + + end + + end + + describe 'ListUserGroups' do + + it 'lists groups and at least one created group appears' do + + group_id1 = "test-group-#{SecureRandom.uuid}" + group_id2 = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id1, name: 'List Test Group One') + create_group(id: group_id2, name: 'List Test Group Two') + + list_resp = @client.common.list_user_groups + expect(list_resp.user_groups).not_to be_nil + expect(list_resp.user_groups).not_to be_empty + + found_ids = list_resp.user_groups.map { |g| g.is_a?(Hash) ? g['id'] : g.id } + expect(found_ids).to include(group_id1).or include(group_id2) + + end + + end + + describe 'ListUserGroupsWithLimit' do + + it 'respects the limit parameter' do + + group_ids = Array.new(3) { "test-group-#{SecureRandom.uuid}" } + group_ids.each_with_index do |gid, i| + + create_group(id: gid, name: "Limit Test Group #{i + 1}") + + end + + limit = 2 + list_resp = @client.common.list_user_groups(limit) + expect(list_resp.user_groups).not_to be_nil + expect(list_resp.user_groups.length).to be <= limit + + end + + end + + describe 'SearchUserGroups' do + + it 'finds a group by name prefix search' do + + unique_prefix = "SearchTest-#{SecureRandom.hex(4)}" + group_id = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id, name: "#{unique_prefix} Group") + + search_resp = @client.common.search_user_groups(unique_prefix) + expect(search_resp.user_groups).not_to be_nil + + found = search_resp.user_groups.any? do |g| + + name = g.is_a?(Hash) ? g['name'] : g.name + name.to_s.start_with?(unique_prefix) + + end + expect(found).to be true + + end + + end + + describe 'AddUserGroupMembers' do + + it 'adds members to an existing group and verifies all are present' do + + user_ids, _resp = create_test_users(3) + group_id = "test-group-#{SecureRandom.uuid}" + + # Create with first member only + create_group(id: group_id, name: 'Member Management Group', member_ids: user_ids[0, 1]) + + # Add remaining members + add_resp = @client.common.add_user_group_members( + group_id, + GetStream::Generated::Models::AddUserGroupMembersRequest.new( + member_ids: user_ids[1..], + ), + ) + expect(add_resp.user_group).not_to be_nil + + # Verify all members are present + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group).not_to be_nil + + members = get_resp.user_group.members || [] + found_ids = members.map { |m| m.is_a?(Hash) ? m['user_id'] : m.user_id } + user_ids.each do |uid| + + expect(found_ids).to include(uid) + + end + + end + + end + + describe 'RemoveUserGroupMembers' do + + it 'removes all members from a group and verifies it is empty' do + + user_ids, _resp = create_test_users(2) + group_id = "test-group-#{SecureRandom.uuid}" + + # Create group with members + create_group(id: group_id, name: 'Remove Members Group', member_ids: user_ids) + + # Verify members are present before removal + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group.members.length).to eq(user_ids.length) + + # Remove all members explicitly by ID (backend requires member_ids) + @client.common.remove_user_group_members( + group_id, + GetStream::Generated::Models::RemoveUserGroupMembersRequest.new( + member_ids: user_ids, + ), + ) + + # Verify members are removed + get_resp2 = @client.common.get_user_group(group_id) + expect(get_resp2.user_group).not_to be_nil + members_after = get_resp2.user_group.members + expect(members_after).to satisfy('be nil or empty') { |m| m.nil? || m.empty? } + + end + + end + + describe 'DeleteUserGroup' do + + it 'deletes a group and verifies a subsequent GET returns an error' do + + group_id = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id, name: 'Group To Delete') + + delete_group(group_id) + + expect do + + @client.common.get_user_group(group_id) + + end.to raise_error(StandardError) + + end + + end + +end From 8465a9c53b070747df977533be541d77bf9468f6 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 11:17:14 +0100 Subject: [PATCH 23/29] style: fix code formatting --- .../chat_user_group_integration_spec.rb | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/spec/integration/chat_user_group_integration_spec.rb b/spec/integration/chat_user_group_integration_spec.rb index 5cd3e47..b2b61e6 100644 --- a/spec/integration/chat_user_group_integration_spec.rb +++ b/spec/integration/chat_user_group_integration_spec.rb @@ -141,17 +141,17 @@ def delete_group(id) it 'lists groups and at least one created group appears' do - group_id1 = "test-group-#{SecureRandom.uuid}" - group_id2 = "test-group-#{SecureRandom.uuid}" - create_group(id: group_id1, name: 'List Test Group One') - create_group(id: group_id2, name: 'List Test Group Two') + group_id_a = "test-group-#{SecureRandom.uuid}" + group_id_b = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id_a, name: 'List Test Group One') + create_group(id: group_id_b, name: 'List Test Group Two') list_resp = @client.common.list_user_groups expect(list_resp.user_groups).not_to be_nil expect(list_resp.user_groups).not_to be_empty found_ids = list_resp.user_groups.map { |g| g.is_a?(Hash) ? g['id'] : g.id } - expect(found_ids).to include(group_id1).or include(group_id2) + expect(found_ids).to include(group_id_a).or include(group_id_b) end @@ -258,9 +258,9 @@ def delete_group(id) ) # Verify members are removed - get_resp2 = @client.common.get_user_group(group_id) - expect(get_resp2.user_group).not_to be_nil - members_after = get_resp2.user_group.members + get_resp_after = @client.common.get_user_group(group_id) + expect(get_resp_after.user_group).not_to be_nil + members_after = get_resp_after.user_group.members expect(members_after).to satisfy('be nil or empty') { |m| m.nil? || m.empty? } end @@ -276,11 +276,7 @@ def delete_group(id) delete_group(group_id) - expect do - - @client.common.get_user_group(group_id) - - end.to raise_error(StandardError) + expect { @client.common.get_user_group(group_id) }.to raise_error(StandardError) end From c8087e09e0bd0f109f130726672e4cadcad94d25 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 11:41:25 +0100 Subject: [PATCH 24/29] test: fine tuning --- .../integration/chat_misc_integration_spec.rb | 12 +++++----- .../integration/chat_user_integration_spec.rb | 23 +++++++------------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb index d3d4cd9..64a57cc 100644 --- a/spec/integration/chat_misc_integration_spec.rb +++ b/spec/integration/chat_misc_integration_spec.rb @@ -267,12 +267,12 @@ # Create channel type with a lower max_message_length so the update below # can demonstrate the value actually changes. The test app plan caps at - # 5000, so stay within that ceiling to avoid silent truncation. + # 4000, so stay within that ceiling to avoid silent truncation. create_resp = @client.make_request(:post, '/api/v2/chat/channeltypes', body: { name: type_name, automod: 'disabled', automod_behavior: 'flag', - max_message_length: 4000, + max_message_length: 3000, }) expect(create_resp.name).to eq(type_name) @created_channel_type_names << type_name @@ -284,19 +284,19 @@ get_resp = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") expect(get_resp.name).to eq(type_name) - # Update channel type — raise to 5000 (plan maximum) to verify the + # Update channel type — raise to 4000 (plan maximum) to verify the # update is applied and the new value is reflected on re-fetch. @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { automod: 'disabled', automod_behavior: 'flag', - max_message_length: 5000, + max_message_length: 4000, typing_events: false, }) # Re-fetch to verify (eventual consistency) sleep(2) updated = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") - expect(updated.max_message_length).to eq(5000) + expect(updated.max_message_length).to eq(4000) # Delete a separate channel type del_name = "testdeltype#{random_string(6)}" @@ -304,7 +304,7 @@ name: del_name, automod: 'disabled', automod_behavior: 'flag', - max_message_length: 5000, + max_message_length: 4000, }) @created_channel_type_names << del_name diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb index 4e0cab1..f760262 100644 --- a/spec/integration/chat_user_integration_spec.rb +++ b/spec/integration/chat_user_integration_spec.rb @@ -210,23 +210,13 @@ def query_users_with_filter(filter, **opts) describe 'DeleteUsers' do - it 'deletes 2 users with retry and polls task until completed' do + it 'deletes 2 users and polls task until completed' do user_ids, _resp = create_test_users(2) - # Remove from tracked list so cleanup doesn't double-delete - user_ids.each { |uid| @created_user_ids.delete(uid) } - - # delete_users is rate-limited to 6 req/min on a fixed 1-minute clock - # window in the Stream backend. Crucially, rejected 429 calls still - # increment the counter, so exponential backoff makes things worse. - # Instead: on a 429, sleep until the next minute boundary (at most 61s) - # to guarantee a fresh window before retrying. resp = nil - last_error = nil 3.times do - last_error = nil resp = @client.common.delete_users( GetStream::Generated::Models::DeleteUsersRequest.new( user_ids: user_ids, @@ -235,17 +225,20 @@ def query_users_with_filter(filter, **opts) conversations: 'hard', ), ) + # Remove from tracked list only after a successful call so that a + # rate-limit failure on a prior attempt still leaves them registered + # for suite cleanup. + user_ids.each { |uid| @created_user_ids.delete(uid) } break + rescue GetStreamRuby::APIError => e raise unless e.message.include?('Too many requests') - last_error = e - sleep(61 - Time.now.sec) + wait = 61 - Time.now.sec + sleep(wait) end - raise last_error if last_error - expect(resp).not_to be_nil task_id = resp.task_id expect(task_id).not_to be_nil From f2891a27b8387508b215542826b0ddc2ee6f1188 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 12:10:00 +0100 Subject: [PATCH 25/29] style: fix code formatting --- spec/integration/chat_user_integration_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb index f760262..7d3aeae 100644 --- a/spec/integration/chat_user_integration_spec.rb +++ b/spec/integration/chat_user_integration_spec.rb @@ -230,7 +230,6 @@ def query_users_with_filter(filter, **opts) # for suite cleanup. user_ids.each { |uid| @created_user_ids.delete(uid) } break - rescue GetStreamRuby::APIError => e raise unless e.message.include?('Too many requests') From 669ad555220faffcccb2e23a696c52cc7ce15f3b Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 19:06:43 +0100 Subject: [PATCH 26/29] test: fine tuning --- .../integration/chat_misc_integration_spec.rb | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb index 64a57cc..99eea9d 100644 --- a/spec/integration/chat_misc_integration_spec.rb +++ b/spec/integration/chat_misc_integration_spec.rb @@ -285,18 +285,16 @@ expect(get_resp.name).to eq(type_name) # Update channel type — raise to 4000 (plan maximum) to verify the - # update is applied and the new value is reflected on re-fetch. - @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { - automod: 'disabled', - automod_behavior: 'flag', - max_message_length: 4000, - typing_events: false, - }) - - # Re-fetch to verify (eventual consistency) - sleep(2) - updated = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") - expect(updated.max_message_length).to eq(4000) + # update is applied. Assert on the update response directly: it is read + # from the writing server's local cache (always fresh) so we avoid the + # eventual consistency window that makes a re-fetch flaky. + update_resp = @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 4000, + typing_events: false, + }) + expect(update_resp.max_message_length).to eq(4000) # Delete a separate channel type del_name = "testdeltype#{random_string(6)}" From 12172d3286a2984b6fdd1f696fc109c21ea2324e Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 20:13:42 +0100 Subject: [PATCH 27/29] test: fine tuning --- spec/integration/chat_test_helpers.rb | 33 ++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb index f863bb1..0f354c4 100644 --- a/spec/integration/chat_test_helpers.rb +++ b/spec/integration/chat_test_helpers.rb @@ -22,16 +22,35 @@ def init_chat_client @created_channel_cids = [] end + def retry_on_rate_limit(max_attempts: 3) + attempts = 0 + begin + yield + rescue GetStreamRuby::APIError => e + raise unless e.message.include?('Too many requests') + + attempts += 1 + raise if attempts >= max_attempts + + wait = 61 - Time.now.sec + puts "⏳ Rate-limited, waiting #{wait}s for window reset (attempt #{attempts}/#{max_attempts})..." + sleep(wait) + retry + end + end + def cleanup_chat_resources # Delete channels (they reference users and must be removed per-spec). @created_channel_cids&.each do |cid| type, id = cid.split(':', 2) - @client.make_request( - :delete, - "/api/v2/chat/channels/#{type}/#{id}", - query_params: { 'hard_delete' => 'true' }, - ) + retry_on_rate_limit do + @client.make_request( + :delete, + "/api/v2/chat/channels/#{type}/#{id}", + query_params: { 'hard_delete' => 'true' }, + ) + end rescue StandardError => e puts "Warning: Failed to delete channel #{cid}: #{e.message}" @@ -149,7 +168,9 @@ def get_or_create_channel(type, id, body = {}) def delete_channel(type, id, hard: false) query_params = hard ? { 'hard_delete' => 'true' } : {} - @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}", query_params: query_params) + retry_on_rate_limit do + @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}", query_params: query_params) + end end def query_channels(body) From 06c757e93ee4c6588da14376d1454e0455e87eca Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 26 Feb 2026 20:55:51 +0100 Subject: [PATCH 28/29] style: fix code formatting --- .../chat_message_integration_spec.rb | 32 ++++++++----------- spec/integration/chat_test_helpers.rb | 4 +++ spec/integration/video_integration_spec.rb | 20 +++--------- 3 files changed, 21 insertions(+), 35 deletions(-) diff --git a/spec/integration/chat_message_integration_spec.rb b/spec/integration/chat_message_integration_spec.rb index 52220d5..803aa54 100644 --- a/spec/integration/chat_message_integration_spec.rb +++ b/spec/integration/chat_message_integration_spec.rb @@ -624,15 +624,12 @@ def undelete_message(message_id, body) it 'verifies error when using both query and message_filter_conditions' do - expect do - - search_messages( - filter_conditions: { 'members' => { '$in' => [@user_1] } }, - query: 'test', - message_filter_conditions: { 'text' => { '$q' => 'test' } }, - ) - - end.to raise_error(GetStreamRuby::APIError) + params = { + filter_conditions: { 'members' => { '$in' => [@user_1] } }, + query: 'test', + message_filter_conditions: { 'text' => { '$q' => 'test' } }, + } + expect { search_messages(**params) }.to raise_error(GetStreamRuby::APIError) end @@ -663,16 +660,13 @@ def undelete_message(message_id, body) it 'verifies error when using offset with next' do - expect do - - search_messages( - filter_conditions: { 'members' => { '$in' => [@user_1] } }, - query: 'test', - offset: 1, - next: SecureRandom.hex(5), - ) - - end.to raise_error(GetStreamRuby::APIError) + params = { + filter_conditions: { 'members' => { '$in' => [@user_1] } }, + query: 'test', + offset: 1, + next: SecureRandom.hex(5), + } + expect { search_messages(**params) }.to raise_error(GetStreamRuby::APIError) end diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb index 0f354c4..79b151d 100644 --- a/spec/integration/chat_test_helpers.rb +++ b/spec/integration/chat_test_helpers.rb @@ -45,11 +45,13 @@ def cleanup_chat_resources type, id = cid.split(':', 2) retry_on_rate_limit do + @client.make_request( :delete, "/api/v2/chat/channels/#{type}/#{id}", query_params: { 'hard_delete' => 'true' }, ) + end rescue StandardError => e puts "Warning: Failed to delete channel #{cid}: #{e.message}" @@ -169,7 +171,9 @@ def get_or_create_channel(type, id, body = {}) def delete_channel(type, id, hard: false) query_params = hard ? { 'hard_delete' => 'true' } : {} retry_on_rate_limit do + @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}", query_params: query_params) + end end diff --git a/spec/integration/video_integration_spec.rb b/spec/integration/video_integration_spec.rb index 6d78511..9f2b101 100644 --- a/spec/integration/video_integration_spec.rb +++ b/spec/integration/video_integration_spec.rb @@ -814,14 +814,8 @@ def new_call_type_name data: { created_by_id: @user_1 }, }) - expect do - - @client.make_request( - :delete, - "/api/v2/video/call/default/#{call_id}/non-existent-session/recordings/non-existent-filename", - ) - - end.to raise_error(GetStreamRuby::APIError) + url = "/api/v2/video/call/default/#{call_id}/non-existent-session/recordings/non-existent-filename" + expect { @client.make_request(:delete, url) }.to raise_error(GetStreamRuby::APIError) end @@ -834,14 +828,8 @@ def new_call_type_name data: { created_by_id: @user_1 }, }) - expect do - - @client.make_request( - :delete, - "/api/v2/video/call/default/#{call_id}/non-existent-session/transcriptions/non-existent-filename", - ) - - end.to raise_error(GetStreamRuby::APIError) + url = "/api/v2/video/call/default/#{call_id}/non-existent-session/transcriptions/non-existent-filename" + expect { @client.make_request(:delete, url) }.to raise_error(GetStreamRuby::APIError) end From 48bf3d7b43861cb95be57e53883ef2bb4e68b636 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Fri, 27 Feb 2026 13:16:20 +0100 Subject: [PATCH 29/29] test: fine tuning --- spec/integration/chat_message_integration_spec.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/integration/chat_message_integration_spec.rb b/spec/integration/chat_message_integration_spec.rb index 803aa54..0980d22 100644 --- a/spec/integration/chat_message_integration_spec.rb +++ b/spec/integration/chat_message_integration_spec.rb @@ -314,7 +314,7 @@ def undelete_message(message_id, body) message: { text: 'Pending message text', user_id: @user_1 }, pending: true, skip_push: true) - rescue StandardError => e + rescue GetStreamRuby::APIError => e if e.message.include?('pending messages not enabled') || e.message.include?('feature flag') skip('Pending messages feature not enabled for this app') end @@ -359,7 +359,7 @@ def undelete_message(message_id, body) filter: { 'message_id' => msg_id }, sort: [], ) - rescue StandardError => e + rescue GetStreamRuby::APIError => e if e.message.include?('feature flag') || e.message.include?('not enabled') skip('QueryMessageHistory feature not enabled for this app') end @@ -378,8 +378,8 @@ def undelete_message(message_id, body) end # Verify text values (descending by default: most recent first) - expect(hist_resp.message_history[0].to_h['text']).to eq('updated text') - expect(hist_resp.message_history[1].to_h['text']).to eq('initial text') + expect(hist_resp.message_history.first.to_h['text']).to eq('updated text 2') + expect(hist_resp.message_history.last.to_h['text']).to eq('initial text') end @@ -403,7 +403,7 @@ def undelete_message(message_id, body) filter: { 'message_id' => msg_id }, sort: [{ 'field' => 'message_updated_at', 'direction' => 1 }], ) - rescue StandardError => e + rescue GetStreamRuby::APIError => e if e.message.include?('feature flag') || e.message.include?('not enabled') skip('QueryMessageHistory feature not enabled for this app') end @@ -486,7 +486,7 @@ def undelete_message(message_id, body) # Undelete begin undel_resp = undelete_message(msg_id, undeleted_by: @user_1) - rescue StandardError => e + rescue GetStreamRuby::APIError => e if e.message.include?('undeleted_by') || e.message.include?('required field') skip('UndeleteMessage requires undeleted_by field not yet in generated request struct') end @@ -510,7 +510,7 @@ def undelete_message(message_id, body) send_resp = send_msg('messaging', channel_id, message: { text: 'Secret message', user_id: @user_1, restricted_visibility: [@user_1] }) - rescue StandardError => e + rescue GetStreamRuby::APIError => e if e.message.include?('private messaging is not allowed') || e.message.include?('not enabled') skip('RestrictedVisibility (private messaging) is not enabled for this app') end