24

我正在尝试为一些需要身份验证的 RSpec 请求设置标头。标题是ACCESS_TOKEN. 无论我如何尝试设置标题,它都不会被设置。我知道该应用程序有效,因为我可以手动测试它,我只是无法让 rspec 测试工作。在此处查看此问题的完整源代码和测试:https ://github.com/lightswitch05/rspec-set-header-example

由于在我的大多数请求规范中都使用了身份验证,因此我创建了支持帮助程序模块来检索访问令牌并将其设置在标头中。下面是我如何尝试设置标题的摘要,请查看我在完整源代码中尝试过的所有内容

# my_app/spec/support/session_helper.rb
module SessionHelper
  def retrieve_access_token
    post api_v1_session_path({email: 'test@example.com', password: 'poor_password'})

    expect(response.response_code).to eq 201
    expect(response.body).to match(/"access_token":".{20}"/)
    parsed = JSON(response.body)
    token = parsed['access_token']['access_token']

    @request.headers['HTTP_ACCESS_TOKEN'] = token
  end
end

一个使用这个帮助器并且应该可以工作的示例请求规范,但总是失败,因为标头永远不会被设置:

# my_app/spec/requests/posts_spec.rb
# ...
context "create" do
  it "creates a post" do
    retrieve_access_token
    post = FactoryGirl.build(:post)

    post api_v1_posts_path(
      post: {
        title: post.title,
        content: post.content
      }
    )

    expect(response.body).to include('"id":')
    expect(response.body).to include('"title":"' + post.title + '"')
    expect(response.body).to include('"content":"' + post.content + '"')
    expect(response.response_code).to eq 201
  end
end

我知道我可以在个人getpost请求中手动设置标头 - 但这不是 API 范围授权的可维护解决方案。想象一下,如果标题名称略有变化,则必须更改每个测试。

4

5 回答 5

41

注意:此答案基于您似乎在请求您尝试在请求规范中运行的每个规范时调用的内容api_v1_session_pathpostSessionsController

有两种方法可以解决我认为您在这里遇到的问题。

解决方案 #1SessionHelper - 您可以在您的或其他一些名为 support/requests_helper.rb 的帮助文件中创建另一个帮助方法(无论您喜欢什么)。我会在support/requests_helper.rb创建另一个助手:

module RequestsHelper
  def get_with_token(path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    get path, params, headers
  end

  def post_with_token(path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    post path, params, headers
  end

  # similarly for xhr..
end

然后在 rails_helper.rb 中:

  # Include the sessions helper
  config.include SessionHelper, type: :request
  # Include the requests helper
  config.include RequestsHelper, type: :request

更改 session_helper.rb:

# my_app/spec/support/session_helper.rb
module SessionHelper
  def retrieve_access_token
    post api_v1_session_path({email: 'test@example.com', password: 'poor_password'})

    expect(response.response_code).to eq 201
    expect(response.body).to match(/"access_token":".{20}"/)
    parsed = JSON(response.body)
    parsed['access_token']['access_token'] # return token here!!
  end
end

现在,您可以像这样更改所有请求规范:

describe Api::V1::PostsController do

  context "index" do
    it "retrieves the posts" do
      get_with_token api_v1_posts_path

      expect(response.body).to include('"posts":[]')
      expect(response.response_code).to eq 200
    end

    it "requires a valid session key" do
      get api_v1_posts_path

      expect(response.body).to include('"error":"unauthenticated"')
      expect(response.response_code).to eq 401
    end
  end
end

解决方案 #2 -将specs/factories/access_token_factory.rb 更改为:

FactoryGirl.define do
  factory :access_token do
    active true
  end

  # can be used when you want to test against expired access tokens:
  factory :inactive_access_token do
    active false
  end
end

现在,将您的所有请求规范更改为使用access_token

describe Api::V1::PostsController do

  context "index" do
    let(:access_token){ FactoryGirl.create(:access_token) }

    it "retrieves the posts" do
      # You will have to send HEADERS while making request like this:
      get api_v1_posts_path, nil, { 'HTTP_ACCESS_TOKEN' => access_token.access_token }

      expect(response.body).to include('"posts":[]')
      expect(response.response_code).to eq 200
    end

    it "requires a valid session key" do
      get api_v1_posts_path

      expect(response.body).to include('"error":"unauthenticated"')
      expect(response.response_code).to eq 401
    end
  end
end

我会选择“解决方案 #1 ”,因为它消除了让您记住HTTP_ACCESS_TOKEN每次要发出此类请求时都发送标头的负担。

于 2014-09-15T12:17:02.737 回答
14

常见的误解是平等对待控制器和请求测试。

最好从阅读控制器规范请求规范开始。如您所见,控制器规范模拟 http 请求,而请求规范执行全栈请求。

你可以在这里找到一些关于为什么你应该编写控制器规范以及在那里测试什么的好文章。虽然编写它们很好,但在我看来,它们不应该触及数据库。

因此,虽然Voxdei 的答案部分有效(在将请求规范更改为控制器规范之后,您设置标头的方式将起作用),但在我看来它没有抓住重点。

在请求规范中,您不能只使用请求/控制器方法,您必须将哈希中的标头作为请求方法的第三个参数传递,即

post '/something', {}, {'MY-HEADER' => 'value'}

您可以做的是存根身份验证,例如:

before do
  allow(AccessToken).to receive("authenticate").and_return(true)
end

然后,您可以在一个规范中测试您的身份验证,以确保它可以正常工作并在过滤其他规范之前使用它。这也可能是更好的方法,因为每次运行需要身份验证的规范时执行额外的请求是相当巨大的开销。

我还在葡萄 gem中发现了非常有趣的拉取请求,它尝试添加默认标头行为,因此如果您真的想在请求规范中使用默认标头,您也可以尝试使用这种方法。

于 2014-09-15T09:13:37.823 回答
4

可能是因为现在 Rspec 如何处理规范文件。它不再自动从文件位置推断规范类型

尝试将此行为设置回您以前知道的

RSpec.configure do |config|
  config.infer_spec_type_from_file_location!
end

或为项目中的每个控制器规范文件在本地设置它

describe MyController, type: :controller do
  # your specs accessing @request
end
于 2014-09-15T08:11:52.757 回答
2

苏里亚的回答是最好的。但是你可以再干一点:

def request_with_user_session(method, path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    send(method, path, params, headers)
end

这里只有一种方法,通过给定的参数调用请求方法method

于 2015-09-12T13:02:36.160 回答
0

我存根验证请求以返回 true 或函数返回的任何值的函数。

ApplicationController.any_instance.stub(:authenticate_request) { true }
于 2015-11-17T09:13:04.250 回答