I'm using RSwag (2.7) to generate API documentation based on tests on Rails 6.1. The problem is that using the same method specified in the official documentation I get an RSpec error.
I need to add a pretty complex request body and the documentation https://github.com/rswag/rswag states you can use
request_body_example value: { some_field: 'Foo' }, name: 'request_example_1', summary: 'A request example'
to specify a body request example for your request.
So I have this test:
post 'Creates holdings in bulk' do
tags 'Holdings'
produces 'application/json'
consumes 'application/json'
request_body_example name: 'some name' summary: 'Request example description', value: { 'some hash' }
it_behaves_like '200_response', 'Holdings'
end
But it raises:
NoMethodError:
undefined method `request_body_example' for RSpec::ExampleGroups::Holdings::Bulk::HoldingsBulk::Post:Class
Did you mean? require_or_load
Is there something I'm doing wrong? It seems like it's the same as the documentation.
Related
I'm new to RoR, a real newbie. But I got a project that need RoR for the backend development. And in this project for the API documentation is using Rswag. and I hit a wall when tried to authorizing the endpoints using JWT, which I want to create the user and the JWT token on the global, similar to what Rspec can do.
But I got a problem when trying to assign a global variable into the rswag tests, I already tried using the before(:each), let() and let!() as well, but they still didn't work which working on the Rspec
require "swagger_helper"
RSpec.describe "Profile API", type: :request do
# I also tried to assign the variables before each test cases
#
# let(:user) { FactoryBot.create(:user, role_id: 3, phone_number: "+6285123456799") }
# --- OR ---
# before(:each) do
# #user = FactoryBot.create(:user, role_id: 3, phone_number: "+6285123456799")
# end
path "/api/v1/profiles" do
post "Create new profile" do
tags "Profile"
consumes "application/json"
produces "application/json"
security [ Bearer: {} ]
parameter name: :profile, in: :body, schema: {
type: :object,
properties: {
first_name: {type: :string},
last_name: {type: :string},
},
required: [:first_name]
}
# I've tried put it here so all the test blocks can access it
# let!(:user) { FactoryBot.create(:user, role_id: 3, phone_number: "+6285123456789") }
response 201, "All fields filled" do
let(:"Authorization") { "Bearer #{auth_header(#user)}" }
let(:profile) { {first_name: "John", last_name: "Doe"} }
run_test! do |response|
expect(response_body).to eq({
"first_name" => "John",
"last_name" => "Doe"
})
end
end
end
end
And here is the error that I got when I tried using let() or let!()
and if I tried to use the Before Block, it doesn't return any errors, but it's also didn't get triggered when I put the byebug in the before block.
currently here are the installed version lists:
Ruby version 3.0.0
Rails version 6.1.1
Rspec-rails gem version 4.0.2
Rswag gem version 2.3.2
And here is the closest answer I can found in the issues, but this doesn't resolve the problem I'm facing now
https://github.com/rswag/rswag/issues/168#issuecomment-454797784
Any bits of help is appreciated, thank you :smile:
I have a Sidekiq worker that reaches out to an external API to get some data back. I am trying to write tests to make sure that this worker is designed and functioning correctly. The worker grabs a local model instance and examines two fields on the model. If one of the fields is nil, it will send the other field to the remote API.
Here's the worker code:
class TokenizeAndVectorizeWorker
include Sidekiq::Worker
sidekiq_options queue: 'tokenizer_vectorizer', retry: true, backtrace: true
def perform(article_id)
article = Article.find(article_id)
tokenizer_url = ENV['TOKENIZER_URL']
if article.content.nil?
send_content = article.abstract
else
send_content = article.content
end
# configure Faraday
conn = Faraday.new(tokenizer_url) do |c|
c.use Faraday::Response::RaiseError
c.headers['Content-Type'] = 'application/x-www-form-urlencoded'
end
# get the response from the tokenizer
resp = conn.post '/tokenize', "content=#{URI.encode(send_content)}"
# the response's body contains the JSON for the tokenized and vectorized article content
article.token_vector = resp.body
article.save
end
end
I want to write a test to ensure that if the article content is nil that the article abstract is what is sent to be encoded.
My assumption is that the "right" way to do this would be to mock responses with Faraday such that I expect a specific response to a specific input. By creating an article with nil content and an abstract x I can mock a response to sending x to the remote API, and mock a response to sending nil to the remote API. I can also create an article with x as the abstract and z as the content and mock responses for z.
I have written a test that generically mocks Faraday:
it "should fetch the token vector on ingest" do
# don't wait for async sidekiq job
Sidekiq::Testing.inline!
# stub Faraday to return something without making a real request
allow_any_instance_of(Faraday::Connection).to receive(:post).and_return(
double('response', status: 200, body: "some data")
)
# create an attrs to hand to ingest
attrs = {
data_source: #data_source,
title: Faker::Book.title,
url: Faker::Internet.url,
content: Faker::Lorem.paragraphs(number: 5).join("<br>"),
abstract: Faker::Book.genre,
published_on: DateTime.now,
created_at: DateTime.now
}
# ingest an article from the attrs
status = Article.ingest(attrs)
# the ingest occurs roughly simultaneously to the submission to the
# worker so we need to re-fetch the article by the id because at that
# point it will have gotten the vector saved to the DB
#token_vector_article = Article.find(status[1].id)
# we should've saved "some data" as the token_vector
expect(#token_vector_article.token_vector).not_to eq(nil)
expect(#token_vector_article.token_vector).to eq("some data")
end
But this mocks 100% of uses of Faraday with :post. In my particular case, I have no earthly idea how to mock a response of :post with a specific body...
It's also possible that I'm going about testing this all wrong. I could be instead testing that we are sending the right content (the test should check what is being sent with Faraday) and completely ignoring the right response.
What is the correct way to test that this worker does the right thing (sends content, or sends abstract if content is nil)? Is it to test what's being sent, or test what we are getting back as a reflection of what's being sent?
If I should be testing what's coming back as a reflection of what's being sent, how do I mock different responses from Faraday depending on the value of something being sent to it/
** note added later **
I did some more digging and thought, OK, let me test that I'm sending the request I expect, and that I'm processing the response correctly. So, I tried to use webmock.
it "should fetch token vector for article content when content is not nil" do
require 'webmock/rspec'
# don't wait for async sidekiq job
Sidekiq::Testing.inline!
request_url = "#{ENV['TOKENIZER_URL']}/tokenize"
# webmock the expected request and response
stub = stub_request(:post, request_url)
.with(body: 'content=y')
.to_return(body: 'y')
# create an attrs to hand to ingest
attrs = {
data_source: #data_source,
title: Faker::Book.title,
url: Faker::Internet.url,
content: "y",
abstract: Faker::Book.genre,
published_on: DateTime.now,
created_at: DateTime.now
}
# ingest an article from the attrs
status = Article.ingest(attrs)
# the ingest occurs roughly simultaneously to the submission to the
# worker so we need to re-fetch the article by the id because at that
# point it will have gotten the vector saved to the DB
#token_vector_article = Article.find(status[1].id)
# we should have sent a request with content=y
expect(stub).to have_been_requested
# we should've saved "y" as the token_vector
expect(#token_vector_article.token_vector).not_to eq(nil)
expect(#token_vector_article.token_vector).to eq("y")
end
But I think that webmock isn't getting picked up inside the sidekiq job, because I get this:
1) Article tokenization and vectorization should fetch token vector for article content when content is not nil
Failure/Error: expect(stub).to have_been_requested
The request POST https://zzzzz/tokenize with body "content=y" was expected to execute 1 time but it executed 0 times
The following requests were made:
No requests were made.
============================================================
If I try to include webmock/rspec in any of the other places, for example, at the beginning of my file, random things start to explode. For example, if I have these lines in the beginning of this spec file:
require 'spec_helper'
require 'rails_helper'
require 'sidekiq/testing'
require 'webmock/rspec'
Then I get:
root#c18df30d6d22:/usr/src/app# bundle exec rspec spec/models/article_spec.rb:174
database: test
Run options: include {:locations=>{"./spec/models/article_spec.rb"=>[174]}}
There was an error creating the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
There was an error removing the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
Which I am guessing is because the test suite is trying to initialize stuff, but webmock is interfering...
I ended up abandoning Faraday and a more complicated test as an approach. I decomposed the worker into both a Service class and a worker. The worker simply invokes the Service class. This allows me to test the service class directly, and then just validate that the worker calls the service class correctly, and that the model calls the worker correctly.
Here's the much simpler service class:
require 'excon'
# this class is used to call out to the tokenizer service to retrieve
# a tokenized and vectorized JSON to store in an article model instance
class TokenizerVectorizerService
def self.tokenize(content)
tokenizer_url = ENV['TOKENIZER_URL']
response = Excon.post("#{tokenizer_url}/tokenize",
body: URI.encode_www_form(content: content),
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
expects: [200])
# the response's body contains the JSON for the tokenized and vectorized
# article content
response.body
end
end
Here's the test to see that we are calling the right destination:
require 'rails_helper'
require 'spec_helper'
require 'webmock/rspec'
RSpec.describe TokenizerVectorizerService, type: :service do
describe "tokenize" do
it "should send the content passed in" do
request_url = "#{ENV['TOKENIZER_URL']}/tokenize"
# webmock the expected request and response
stub = stub_request(:post, request_url).
with(
body: {"content"=>"y"},
headers: {
'Content-Type'=>'application/x-www-form-urlencoded',
}).
to_return(status: 200, body: "y", headers: {})
TokenizerVectorizerService.tokenize("y")
expect(stub).to have_been_requested
end
end
end
I am current developing an API endpoint in rails. I want to be sure the endpoint response with the correct error status if the data I need is invalid. I need an array of ids. One of the invalid values is an empty array.
Valid
{ vendor_district_ids: [2, 4, 5, 6]}
Invalid
{ vendor_district_ids: []}
Request Spec with RSpec
So I want to have a request spec to control my behaviour.
require 'rails_helper'
RSpec.describe Api::PossibleAppointmentCountsController, type: :request do
let(:api_auth_headers) do
{ 'Authorization' => 'Bearer this_is_a_test' }
end
describe 'POST /api/possible_appointments/counts' do
subject(:post_request) do
post api_my_controller_path,
params: { vendor_district_ids: [] },
headers: api_auth_headers
end
before { post_request }
it { expect(response.status).to eq 400 }
end
end
As you can see I use an empty array in my param inside the subject block.
Value inside the controller
In my controller I am fetching the data with
params.require(:vendor_district_ids)
and the value is the following
<ActionController::Parameters {"vendor_district_ids"=>[""], "controller"=>"api/my_controller", "action"=>"create"} permitted: false>
The value of vendor_district_ids is an array with an empty string. I do not have the same value when I make a post with postman.
Value with postman
If I post
{ "vendor_district_ids": [] }
the controller will receive
<ActionController::Parameters {"vendor_district_ids"=>[], "controller"=>"api/my_controller", "action"=>"create"} permitted: false>
And here is the array empty.
Question
Am I doing something wrong inside the request spec or is this a bug from RSpec?
Found the answer!
Problem
The problem is found inside Rack's query_parser not actually inside rack-test as the previous answer indicates.
The actual translation of "paramName[]=" into {"paramName":[""]} happens in Rack's query_parser.
An example of the problem:
post '/posts', { ids: [] }
{"ids"=>[""]} # By default, Rack::Test will use HTTP form encoding, as per docs: https://github.com/rack/rack-test/blob/master/README.md#examples
Solution
Convert your params into JSON, by requiring the JSON gem into your application using 'require 'json' and appending your param hash with .to_json.
And specifying in your RSPEC request that the content-type of this request is JSON.
An example by modifying the example above:
post '/posts', { ids: [] }.to_json, { "CONTENT_TYPE" => "application/json" }
{"ids"=>[]} # explicitly sending JSON will work nicely
That is actually caused by rack-test >= 0.7.0 [1].
It converts empty arrays to param[]= which is later decoded as [''].
If you try running the same code with e.g. rack-test 0.6.3 you will see that vendor_district_ids isn't added to the query at all:
# rack-test 0.6.3
Rack::Test::Utils.build_nested_query('a' => [])
# => ""
# rack-test >= 0.7.0
Rack::Test::Utils.build_nested_query('a' => [])
# => "a[]="
Rack::Utils.parse_nested_query('a[]=')
# => {"a"=>[""]}
[1] https://github.com/rack-test/rack-test/commit/ece681de8ffee9d0caff30e9b93f882cc58f14cb
For everyone wondering — there's a shortcut solution:
post '/posts', params: { ids: [] }, as: :json
I'm building an API in Rails 4 using rspec_api_documentation and have been really impressed. Having opted to use DoorKeeper to secure my endpoints, I'm successfully able to test this all from the console, and got it working.
Where I am having difficulty now is how to spec it out, and stub the token.
DoorKeeper's documentation suggests using the following:
describe Api::V1::ProfilesController do
describe 'GET #index' do
let(:token) { stub :accessible? => true }
before do
controller.stub(:doorkeeper_token) { token }
end
it 'responds with 200' do
get :index, :format => :json
response.status.should eq(200)
end
end
end
However, I've written an acceptance test in line with rspec_api_documentation. This is the projects_spec.rb that I've written:
require 'spec_helper'
require 'rspec_api_documentation/dsl'
resource "Projects" do
header "Accept", "application/json"
header "Content-Type", "application/json"
let(:token) { stub :accessible? => true }
before do
controller.stub(:doorkeeper_token) { token }
end
get "/api/v1/group_runs" do
parameter :page, "Current page of projects"
example_request "Getting a list of projects" do
status.should == 200
end
end
end
When I run the test I get the following:
undefined local variable or method `controller' for #<RSpec::Core
I suspect this is because it's not explicitly a controller spec, but as I said, I'd rather stick to this rspec_api_documentation way of testing my API.
Surely someone has had to do this? Is there another way I could be stubbing the token?
Thanks in advance.
I had the same problem and I created manually the access token with a specified token. By doing that, I was then able to use my defined token in the Authorization header :
resource "Projects" do
let(:oauth_app) {
Doorkeeper::Application.create!(
name: "My Application",
redirect_uri: "urn:ietf:wg:oauth:2.0:oob"
)
}
let(:access_token) { Doorkeeper::AccessToken.create!(application: oauth_app) }
let(:authorization) { "Bearer #{access_token.token}" }
header 'Authorization', :authorization
get "/api/v1/group_runs" do
example_request "Getting a list of projects" do
status.should == 200
end
end
end
I wouldn't recommend stubbing out DoorKeeper in an rspec_api_documentation acceptance test. One of the benefits of RAD is seeing all of the headers in the examples that it generates. If you're stubbing out OAuth2, then people reading the documentation won't see any of the OAuth2 headers while they're trying to make a client.
I'm also not sure it's possible to do this nicely. RAD is very similar to a Capybara feature test and a quick search makes it seem difficult to do.
RAD has an OAuth2MacClient which you can possibly use, here.
require 'spec_helper'
resource "Projects" do
let(:client) { RspecApiDocumentation::OAuth2MACClient.new(self) }
get "/api/v1/group_runs" do
example_request "Getting a list of projects" do
status.should == 200
end
end
end
I'm using rspec, cucumber and capybara and I'm looking for a way to test that a malicious user can't hack a form then post to an url he/she doesn't have permission to. I have my permissions set up in cancan such that this "should" work, however, the only way I can test it is by hacking a form myself.
How can I automate this sort of testing? With webrat I could do this in a unit test with rspec with something like
put :update, :user_id => #user.id, :id => #user_achievement.id
response.should contain("Error, you don't have permission to access that!")
In capybara, however, visit only does get's it seems. I can't find a way to do this, I've googled everwhere.
Any help would be much appreciated,
Thanks
I think you can do this with rack-test
https://github.com/brynary/rack-test
in your Gemfile:
gem 'rack-test'
in your env.rb file
module CapybaraApp
def app; Capybara.app; end
end
World(CapybaraApp)
World(Rack::Test::Methods)
step defintions somewhere:
When /^I send a POST request to "([^"]*)"$/ do |path|
post path
end
Most of what I learned came from here: http://www.anthonyeden.com/2010/11/testing-rest-apis-with-cucumber-and-rack-test
UPDATE: I think you can skip the changes to your env.rb file with newer versions of Rails and/or Cucumber (not sure which, I just don't do that part on my newer projects and it works fine)
Same as #Josh Crews I've largely based this off of: http://www.anthonyeden.com/2010/11/testing-rest-apis-with-cucumber-and-rack-test/#comment-159. But there are two notable exceptions: 1) I test the actual response body, 2) I demonstrate how to test a POST request. Here's an example using Rails 3.0.9:
Steps:
# features/step_definitions/api_step.feature
When /^I send a GET request to "([^\"]*)"$/ do |url|
authorize(User.last.email, "cucumber")
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'
get url
end
When /^I send a POST request to "([^\"]*)" with:$/ do |url, body|
authorize(User.last.email, "cucumber")
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'
post url, body
end
Then /^the JSON response should have (\d+) "([^\"]*)" elements$/ do |number_of_children, name|
page = JSON.parse(last_response.body)
page.map { |d| d[name] }.length.should == number_of_children.to_i
end
Then /^I should receive the following JSON response:$/ do |expected_json|
expected_json = JSON.parse(expected_json)
response_json = JSON.parse(last_response.body)
response_json.should == expected_json
end
Then /^I should receive the following JSON object response:$/ do |expected_json|
expected_json = JSON.parse(expected_json)
response_json = JSON.parse(last_response.body)
if expected_json['id'] == 'RESPONSE_ID'
expected_json['id'] = response_json['id']
end
response_json.should == expected_json
end
Feature:
# features/api/some_feature.feature
Feature: Users API
Background:
Given the following users exist:
| id | name |
| 1 | Joe |
| 2 | Sue |
| 3 | Paul |
Scenario: Index action
When I send a GET request to "/users/"
Then the JSON response should have 3 "user" elements
And I should receive the following JSON response:
"""
[
{
"id":1,
"name":"Joe"
},
{
"id":2,
"name":"Sue"
},
{
"id":3,
"name":"Paul"
}
]
"""
Scenario: Create action
When I send a POST request to "/users/" with:
"""
{
"name":"Polly"
}
"""
Then I should receive the following JSON object response:
"""
{
"id":"RESPONSE_ID",
"name":"Polly"
}
"""
And I send a GET request to "/users/"
And the JSON response should have 4 "user" elements