Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--require spec_helper
--format documentation
--profile 25
Comment thread
nijeesh-stream marked this conversation as resolved.
58 changes: 46 additions & 12 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
require 'faraday'

describe StreamChat::Client do
# Block-form retry helper. Kept under the original `loop_times` name to
# preserve backward compatibility with existing call sites. New tests
# should prefer the `eventually` RSpec matcher defined above, which reads
# as an eventual-consistency contract rather than a retry loop.
def loop_times(times)
loop do
begin
Expand All @@ -20,16 +24,44 @@ def loop_times(times)
end
end

# Hard-delete fellowship leftovers from prior CI runs whose after(:all)
# didn't fire. `delete_users` is idempotent so this is safe to re-run.
# Without it the shared test app accumulates Frodos / Sams / Gandalfs /
# Legolases forever and other tests that query users by name match sediment.
def cleanup_fellowship_leftovers!(client)
names = ['Frodo Baggins', 'Samwise Gamgee', 'Gandalf the Grey', 'Legolas']
leftover = client.query_users({ 'name' => { '$in' => names } })
leftover_ids = leftover['users'].map { |u| u['id'] }
return if leftover_ids.empty?

client.delete_users(leftover_ids,
user: StreamChat::HARD_DELETE,
messages: StreamChat::HARD_DELETE)
rescue StandardError
# Best-effort: a failed cleanup must not block tests; the per-run race
# tag below isolates concurrent runs that race past this point.
end

before(:all) do
@client = StreamChat::Client.from_env
cleanup_fellowship_leftovers!(@client)
end

before(:all) do
@created_users = []

# Per-run race markers so concurrent CI runs (and the seconds-window between
# the active cleanup above and the upserts below) don't collide on
# race=Hobbit / Elf / Istari lookups.
@hobbit_race = "Hobbit-#{SecureRandom.alphanumeric(8)}"
@elf_race = "Elf-#{SecureRandom.alphanumeric(8)}"
@istari_race = "Istari-#{SecureRandom.alphanumeric(8)}"

@fellowship_of_the_ring = [
{ id: SecureRandom.uuid, name: 'Frodo Baggins', race: 'Hobbit', age: 50 },
{ id: SecureRandom.uuid, name: 'Samwise Gamgee', race: 'Hobbit', age: 38 },
{ id: SecureRandom.uuid, name: 'Gandalf the Grey', race: 'Istari' },
{ id: SecureRandom.uuid, name: 'Legolas', race: 'Elf', age: 500 }
{ id: SecureRandom.uuid, name: 'Frodo Baggins', race: @hobbit_race, age: 50 },
{ id: SecureRandom.uuid, name: 'Samwise Gamgee', race: @hobbit_race, age: 38 },
{ id: SecureRandom.uuid, name: 'Gandalf the Grey', race: @istari_race },
{ id: SecureRandom.uuid, name: 'Legolas', race: @elf_race, age: 500 }
]

@legolas = @fellowship_of_the_ring[3][:id]
Expand Down Expand Up @@ -588,7 +620,7 @@ def loop_times(times)
end

it 'queries users' do
response = @client.query_users({ 'race' => { '$eq' => 'Hobbit' } }, sort: { 'age' => -1 })
response = @client.query_users({ 'race' => { '$eq' => @hobbit_race } }, sort: { 'age' => -1 })
expect(response['users'].length).to eq 2
expect([50, 38]).to eq(response['users'].map { |u| u['age'] })
end
Expand Down Expand Up @@ -1025,20 +1057,22 @@ def loop_times(times)
expect(cmd['description']).to eq 'I am testing'
end

# Each follow-up step uses the `eventually` matcher to absorb the
# backend's eventual-consistency lag between Create / Get / Update /
# Delete — on a cold shared CI app the next call regularly hits
# "the command ... does not exist" until the write propagates.
it 'get that command' do
cmd = @client.get_command(@cmd)
expect(cmd['name']).to eq @cmd
expect(cmd['description']).to eq 'I am testing'
expect { @client.get_command(@cmd) }
.to eventually(include('name' => @cmd, 'description' => 'I am testing'))
end

it 'update that command' do
cmd = @client.update_command(@cmd, { description: 'I tested' })['command']
expect(cmd['name']).to eq @cmd
expect(cmd['description']).to eq 'I tested'
expect { @client.update_command(@cmd, { description: 'I tested' })['command'] }
.to eventually(include('name' => @cmd, 'description' => 'I tested'))
end

it 'delete that command' do
@client.delete_command(@cmd)
expect { @client.delete_command(@cmd) }.to eventually(be_truthy)
end

it 'list commands' do
Expand Down
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
formatter SimpleCov::Formatter::Console
add_filter '/spec/'
end

# Autoload everything under spec/support — custom RSpec matchers, shared
# contexts, fixtures, etc.
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
47 changes: 47 additions & 0 deletions spec/support/matchers/eventually.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

# Block matcher that retries `expect { ... }` against an inner matcher until
# it passes or the budget runs out. Designed for the eventually-consistent
# bits of the shared-CI backend (e.g. create_command -> get_command races).
#
# expect { client.get_command(name) }.to eventually(include('name' => name))
#
# Tweak the budget with chainable `.with_retries(n)` / `.every(seconds)`.
RSpec::Matchers.define :eventually do |inner_matcher|
supports_block_expectations

match do |block|
@retries = (defined?(@retries_value) && @retries_value) || 3
@interval = (defined?(@interval_value) && @interval_value) || 1
@last_actual = nil
@last_error = nil
attempts = 0
loop do
begin
@last_actual = block.call
break true if inner_matcher.matches?(@last_actual)
rescue StandardError, RSpec::Expectations::ExpectationNotMetError => e
@last_error = e
end
attempts += 1
break false if attempts >= @retries

sleep(@interval)
end
end

chain :with_retries do |n|
@retries_value = n
end

chain :every do |seconds|
@interval_value = seconds
end

failure_message do |_block|
msg = "expected block to eventually #{inner_matcher.description}"
msg += "; last value: #{@last_actual.inspect}" unless @last_actual.nil?
msg += "; last error: #{@last_error.message}" if @last_error
msg
end
end
30 changes: 30 additions & 0 deletions spec/support/timing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

# Per-test wall-clock timing emitted to STDOUT so CI logs surface which
# example is in flight when a runner stalls. The default `--format
# documentation` reporter prints test names but not timing; `--profile`
# only summarises at the end (no help when the run hangs before reaching
# the summary). This hook fills the gap: every example logs
# `[timing] start ...` on enter and `[timing] N.NN s ...` on exit with
# its full description path, so an unattended runner stuck mid-test
# leaves a clear breadcrumb.
RSpec.configure do |config|
config.before(:each) do |example|
example.metadata[:timing_started_at] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
warn "[timing] start #{example.full_description}"
end

config.after(:each) do |example|
started_at = example.metadata[:timing_started_at]
next unless started_at

elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
status = example.exception ? 'fail' : 'pass'
warn format(
'[timing] %<elapsed>.2f s %<status>s %<description>s',
elapsed: elapsed,
status: status,
description: example.full_description
)
end
end
Loading