podia / stimulus_reflex_testing

MIT License
45 stars 6 forks source link

Session Double - Issue with "Impersonate" #14

Open Laykou opened 3 years ago

Laykou commented 3 years ago

We use

and stimulus_reflex everything runs smoothly in a standard reflex calls via web browser.

We ran into issue with rspec tests using stimulus_reflex_testing gem:

RSpec::Mocks::MockExpectationError: #<Double :session> received unexpected message :[] with (:impersonated_user_id)

  0) TransactionReflex#workflow updates transaction data
     Failure/Error: - if can? :edit, workflow
       #<Double :session> received unexpected message :[] with (:impersonated_user_id)
     # /Users/laykou/.rvm/gems/ruby-3.0.1/gems/pretender-0.3.4/lib/pretender.rb:36:in `block in impersonates'
     # /Users/laykou/.rvm/gems/ruby-3.0.1/gems/cancancan-3.2.2/lib/cancan/controller_additions.rb:354:in `current_ability'
     # /Users/laykou/.rvm/gems/ruby-3.0.1/gems/cancancan-3.2.2/lib/cancan/controller_additions.rb:377:in `can?'
     # /Users/laykou/.rvm/gems/ruby-3.0.1/gems/cancancan-3.2.2/lib/cancan/controller_additions.rb:298:in `can?'
     # ./app/views/transaction_tabs/actions/_action_form.html.haml:16:in `block in _app_views_transaction_tabs_actions__action_form_html_haml__1791840269393427833_188340'

It's probably because session uses Double instead of real object.

What would be your suggestion for workaround or fix, please/

Follow up discussion on Discord:

jasoncharnes commented 3 years ago

Hey @Laykou, a double for the session is definitely something I'd like to revisit when time allows in the future.

In the meantime, a workaround might be attempting to stub the session as mentioned here.

Laykou commented 3 years ago

I'm not sure how to do that correctly. I tried adding

before do
  session = double(:session, impersonated_user_id: nil)

  allow(reflex).to receive(:session) { session } 
end

also I tried adding:

allow(controller).to receive(:current_user).and_return(user)

but there is no instance of controller. I'm not sure how I can access/override the view.

Kindly would you please help me to understand how this "double thing" should work or what should I read/understand if I'd like to help to solve this "session" issue?

jasoncharnes commented 3 years ago

Would you mind providing what reflex is in this example?

before do
  session = double(:session, impersonated_user_id: nil)

  allow(reflex).to receive(:session) { session } 
end

It's possible my solution may not be working. If it isn't, we can work together to find some type of solution here. 👍

Laykou commented 3 years ago

It's huge project (private), not sure if I can simply just copy-paste some parts to create new project where I can replicate the issue. Maybe easier would be to have screen-sharing call where we can figure it out and maybe put some working example for future generations? :)

jasoncharnes commented 3 years ago

I understand that. 👍

When you do allow(reflex) is reflex made with build_reflex?

Maybe easier would be to have screen-sharing call where we can figure it out and maybe put some working example for future generations? :)

I would be up for this, too! It might be later in the week, for me.

Laykou commented 3 years ago

Yes it is created with build_reflex. Here I tried to copy-paste important parts of the rspec

RSpec.describe TransactionReflex, type: :reflex do
  subject { reflex.run(:submit) }

  let(:transaction) { create :transaction } 
  let(:params) { { transaction: {} } }
  let(:reflex) { build_reflex(url: transaction_url(transaction), params: params, connection: { current_user: user }) }

  before do
    reflex.element.dataset.transaction_signed_id = transaction.to_persisted_sgid.to_s
    reflex.element.dataset.permission_signed_id = create_permission.to_persisted_sgid.to_s
    reflex.element.dataset.reflex = 'Transaction#submit'

    subject
  end

  it 'updates transaction data' do
      expect(reflex.get(:transaction).data).to eq({ foo: :bar})
      expect(subject).to morph("#transaction_#{transaction.id}")
    end
end

Somewhere there in the StimulusReflex and in some service class there is need to render partial using from builder

Do not judge why we call view from service class 😋 , but it's necessary in the project:

# builder is FormBuilder object

context = builder.instance_variable_get '@template' # access the view
context.try(:current_user)     # this should be equivalent to call `current_user` in controller or view

This calling for current_user fails (inside Reflex) when having also prentender gem

jasoncharnes commented 3 years ago

@Laykou I'm not 100% sure this will solve your issue because I haven't tested with pretender or cancancan, but I think it's worth a try!

What I'm trying here is ~probably~ not good. I've created a branch where I've replaced the double with a class that defines load! (which SR needs) and then exposes a simple data store for []= and [].

Would you try pointing to the session-double branch either directly by Github or pulling it down locally? I'm able to set/read session via my reflexes this way.

This is kind of a hack, but does appear to work so far.


This is an issue (since I started this testing project) that I haven't been able to figure out. I don't think a proper session store gets setup in test environments, so when SR calls load! it's a hash instead of a SessionStore object and we get an undefined method load! for Hash. (I could be wrong, but this is my theory.)

We're still using a cookie store for the session, so we haven't attempted to change the session in our Reflexes yet, hence why we haven't encountered this issue, yet.

Please let me know how it goes!

Laykou commented 3 years ago

Thank you very much! I'll let my team to try this and we'll see how it goes

MiselAdemi commented 3 years ago

Hi @jasoncharnes I am developer from Laykou's team. I was testing this branch I here is what error I am getting at this moment. We are using devise for user authentication.

Failure/Error: @request.env['action_controller.instance'] = @controller

NoMethodError:
       undefined method `env' for nil:NilClass
jasoncharnes commented 3 years ago

@MiselAdemi thanks for testing the branch out. Sorry to hear you're still running into issues.

I put Devise into a test project and authenticated all my controllers.

class ApplicationController < ActionController::Base
  before_action :authenticate_user!
end

I was able to run tests without encountering the error.

require "rails_helper"

RSpec.describe ExampleReflex, type: :reflex do
  it { build_reflex(method_name: "action").run }
end

Are you able to provide any more context/information? I'm glad to keep working with you to try and resolve this. I'm a bit at a loss, currently, for what else we can try.

Laykou commented 2 years ago

Sorry for late response. We got finally back to this topic.

I tried with the double branch. We use https://github.com/ankane/pretender which eventually delegates calling to Devise's current_user method:

     Devise::MissingWarden:
       Devise could not find the `Warden::Proxy` instance on your request environment.
       Make sure that your application is loading Devise and Warden as expected and that the `Warden::Manager` middleware is present in your middleware stack.
       If you are seeing this on one of your tests, ensure that your tests are either executing the Rails middleware stack or that your tests are using the `Devise::Test::ControllerHelpers` module to inject the `request.env['warden']` object for you.
     # /Users/laykou/.rvm/gems/ruby-3.0.2/gems/devise-4.8.0/lib/devise/controllers/helpers.rb:143:in `warden'
     # /Users/laykou/.rvm/gems/ruby-3.0.2/gems/devise-4.8.0/lib/devise/controllers/helpers.rb:126:in `current_user'
     # /Users/laykou/.rvm/gems/ruby-3.0.2/gems/pretender-0.3.4/lib/pretender.rb:52:in `block in impersonates'
     # /Users/laykou/.rvm/gems/ruby-3.0.2/bundler/gems/userstamp-207aa52536f4/lib/userstamp.rb:35:in `set_stamper'
     # ./app/reflexes/application_reflex.rb:15:in `block in <class:ApplicationReflex>'

it eventually calls this: request.env['warden'] or raise MissingWarden from here https://github.com/heartcombo/devise/blob/main/lib/devise/controllers/helpers.rb#L143

Is this fake double or fake session mocking the request.env variable and the warden key?