3

Rails 5.2 I have the following ApplicationCable::Connection ruby file:

module ApplicationCable
  class Connection < ActionCable::Connection::Base

    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

    def find_verified_user
      if verified_user = env['warden'].user
        verified_user
      else
        message = "The user is not found. Connection rejected."
        logger.add_tags 'ActionCable', message  
        self.transmit error: message 
        reject_unauthorized_connection
      end
    end
  end
end

I want to test this setup and and using the following RSpec test:

require 'rails_helper.rb'

RSpec.describe ApplicationCable::Connection, type: :channel do

  it "successfully connects" do
    connect "/cable", headers: { "X-USER-ID" => 325 }
    expect(connection.user_id).to eq 325
  end
end

Which fails with:

Failure/Error: if verified_user = env['warden'].user

NoMethodError: undefined method `[]' for nil:NilClass

So I want to stub out the env['warden'].user code and return an id of 325. I tried the following:

allow(env['warden']).to receive(:user).and_return(325)

But that produced the following error:

undefined local variable or methodenv'

How can I test this class?

chell
  • 7,646
  • 16
  • 74
  • 140

2 Answers2

8

Try this:

require 'rails_helper.rb'

RSpec.describe ApplicationCable::Connection, type: :channel do

   let(:user)    { instance_double(User, id: 325) }
   let(:env)     { instance_double('env') }

  context 'with a verified user' do

     let(:warden)  { instance_double('warden', user: user) } 

    before do
      allow_any_instance_of(ApplicationCable::Connection).to receive(:env).and_return(env)
      allow(env).to receive(:[]).with('warden').and_return(warden)
    end

    it "successfully connects" do
      connect "/cable", headers: { "X-USER-ID" => 325 }
      expect(connect.current_user.id).to eq 325
    end

  end

  context 'without a verified user' do

    let(:warden)  { instance_double('warden', user: nil) }

    before do
      allow_any_instance_of(ApplicationCable::Connection).to receive(:env).and_return(env)
      allow(env).to receive(:[]).with('warden').and_return(warden)
    end

    it "rejects connection" do
      expect { connect "/cable" }.to have_rejected_connection
    end

  end
end
chell
  • 7,646
  • 16
  • 74
  • 140
aridlehoover
  • 3,139
  • 1
  • 26
  • 24
  • 1
    Thank you for your code. I tried this and got the following error: NameError: uninitialized constant Connection – chell Dec 19 '18 at 04:14
  • Updated. `Connection` should have been `ApplicationCable::Connection`. And, I fixed another issue with the `env` mock. – aridlehoover Dec 19 '18 at 06:45
  • I made the changes and now I'm getting ApplicationCable::Connection does not implement: env – chell Dec 19 '18 at 08:51
  • I think that I might need an instance of ApplicationCable::Connection to be stubbed. – chell Dec 19 '18 at 09:05
  • It's smelly, but try `allow_any_instance_of`. If that doesn't do it, then I'll need to see more of your code to see where `env` is being called. – aridlehoover Dec 19 '18 at 11:44
  • I tried it and it returns nil. It's as if it's not using the mock in the test. – chell Dec 20 '18 at 02:29
  • env is not being called anywhere else in the code though. So I don't know what else to show you to help figure out how to mock or stub this method call. – chell Dec 20 '18 at 02:49
  • 1
    I have edited your answer with code that I modified from your answer to have passing tests. I don't know how to work around the smelly allow_any_instance_of part yet. The two tests pass as expected now. I hope you don't mind me editing the code you wrote. – chell Dec 20 '18 at 03:26
  • 1
    Glad you got it working! And, I'm glad I was able to help! I wouldn't worry about the smell. Controllers (and apparently Cables) are notoriously hard to mock because Rails controls the instantiation, so you cannot inject test doubles. That's why RSpec has `allow_any_instance_of`. – aridlehoover Dec 20 '18 at 06:54
1

Here's a good explanation for your issue https://stackoverflow.com/a/17050993/299774

The question was about controller tests, but it's really similar.

I also don't think you should access low level env['warden'] in your controller. What if the gem authors decide to change that - you'd have to fix your app. Probably warden objects are initialized with this config, and there should be an object available (just not necessary when you run your specs - as described in the link above).

Greg
  • 5,862
  • 1
  • 25
  • 52
  • Sorry - I keep mentioning controller, and this is about action cable. But conceptually these are the same layer, so IMO we can talk about them using the "concept" of a controller. – Greg Dec 19 '18 at 11:36
  • Thanks meta. I have setup my ApplicationCable::Connection according to the practices outlined on many sites such as https://www.sitepoint.com/create-a-chat-app-with-rails-5-actioncable-and-devise/. I just wanted to know if there was a way to test it by stubbing out this method. I think the link you sent is relevant and probably the reason why I won't be able to do this tests. – chell Dec 20 '18 at 02:27