Goliath + Redis: Testing sequence of requests - ruby

I wrote an endpoint that has two operations: PUT and GET. GET retrieves what was PUT, so a good way to ensure both work would be to test them in sequence.
My cURL testing tells me the service works. However, I can't get my test that does the same thing to pass!
it "accepts authenticated PUT data" do
attributes1 = {
user: {
lat: '12.34',
lng: '56.78'
}
}
with_api(Location, { log_stdout: true, verbose: true }) do
put_request({ query: { auth_token: 'abc' }, body: attributes1.to_json }) do |client|
client.response_header.status.must_equal 201
get_request({ query: { auth_token: 'abc' } }) do |client|
client.response_header.status.must_equal 200
client.response.must_match_json_expression([attributes1[:user]])
end
end
end
end
Note that PUT accepts JSON in the body, and GET returns JSON. The service uses Redis as the datastore, and I use mock_redis to mock it.
test_helper.rb:
$redis = MockRedis.new
class Goliath::Server
def load_config(file = nil)
config['redis'] = $redis
end
end
top of spec:
before do
$redis.flushdb
end
The GET should retrieve what was just PUT, but instead it retrieves JSON "null".
Am I doing anything obviously wrong? Perhaps with how I am passing along body data to the request?
It would definitely help to get some better logging when my tests are running. I tried the { log_stdout: true, verbose: true } options, but this still only seems to output basic INFO logging from the Goliath server, which doesn't tell me anything about data and params.
Any help would be appreciated!

Related

Testing External Api in ruby using rspec

I am working on a project where I have to test an external api which is making call to github.
Let's take the code below as example, the class below does not have intializer
Class CodeReviewSignedOff
def pullrequestreview(github)
#the following line return a json response, it require two parameter a string and a number
github.client.pull_request_reviews("string",number).each do |github|
#block of code which perform some operation
end
return count #return integer response
end
end
As you can see my class CodeReviewSignedOff relies on the github object which is defined in my existing project and using the object to make external API call.
But the thing is, I do not want to actually call the API.
I want stub request github.client.pull_request_reviews
allow(github).to receive_message_chain(:client,:pull_request_review_requests).and_return json
My question here is,
How should I return the json response because if I include it in the double quote it will be interpreted as string.
My second question
How do I test my pullrequestreview function.
github=double #I am creating a fake github
object=CodeReviewSignedOff.new
expect(pullrequestreview).with(double).and_return(3)
Is the above logic correct?
Good question!
Disclaimer: I'm still learning about RoR, but I can try to answer this question as best as I can. If anyone has suggestions about how I can improve my response, please let me know!
Testing a class method (using stubs)
So, in order to test your method called "pullrequestreview", you'll need to stub a method within the Github::Client class. I personally have not worked with GitHub's API, so let's assume that it is a static class with a few class methods (including pull_request_reviews).
(Note: I renamed "pullrequestreview" to pull_request_review, because it is more idiomatic in Ruby)
In the example below, I stubbed Github::Client.pull_request_reviews, so that it will return res_data whenever it is called:
Github::Client.stubs(:pull_request_reviews).returns(res_data)
Next, I use a "factory" to generate a github_client argument to pass into code_review_sign_off.pull_request_review. Since the GitHub method is stubbed, it doesn't really matter what I pass into it. In more complex test scenarios, it might be a good idea to make use of a factory bot gem to build out different GitHub related objects (with different trait options). For example, using a factory, you could define a new github_client with a "pull_request" trait like so:
github_client = create(:github_client, :with_pull_request)
In your post, code_review_sign_off.pull_request_review is expecting an iterable object from GitHub. Let's assume that code_review_sign_off.pull_request_review returns the number of elements within the array. Since I have control over GitHub's response, I know that only one element will be returned, so the result should be 1, which I assert using:
assert_equal pull_request_review_response, 1
So, in general when testing, you want to issolate the function that you're interested in and use stubs to keep everything else constant.
require 'test_helper'
class CodeReviewSignedOffTest < ActionDispatch::IntegrationTest
test 'with CodeReviewSignedOff method' do
res_data = [{
:id => 1,
:data => "some_data"
}]
Github::Client.stubs(:pull_request_reviews).returns(res_data)
github_client = create(:github_client, :with_pull_request)
code_review_sign_off = CodeReviewSignedOff.new
pull_request_review_response = code_review_sign_off.pull_request_review(github_client)
...
assert_equal pull_request_review_response, 1
end
end
(Note: I'm using the mocha/minitest in the examples)
Bonus: Mocking an API endpoint
So, first, I am going to assume you have your test environment for RSpec properly configured (you ran rails generate rspec:install command) and you have the appropriate /spec directory.
The first step to mocking an API endpoint is to stub the calls to the actual endpoints by using a gem like WebMock. So, install the gem and add require 'webmock/rspec' to /spec/spec_helper.rb
Within /spec/spec_helper.rb you'll need to define a few things,
First, add the following to ensure that you're not continuing to make real calls to the API:
WebMock.disable_net_connect!(allow_localhost: true)
Next, you'll need a stub for each API endpoint that you're mocking. For example, in your case, you'd need to define something like:
RSpec.configure do |config|
config.before(:each) do
pull_request_review_response = [
{
"id": 80,
"node_id": "MDE3OlB1bGxSZXF1ZXN0UmV2aWV3ODA=",
"user": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"body": "Here is the body for the review.",
"state": "APPROVED",
"html_url": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80",
"pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/12",
"_links": {
"html": {
"href": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80"
},
"pull_request": {
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/12"
}
},
"submitted_at": "2019-11-17T17:43:43Z",
"commit_id": "ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091",
"author_association": "COLLABORATOR"
}
]
owner = 'octocat',
repo = 'hello-world',
pull_number = 42
stub_request(:get, "https://api.github.com/repos/#{owner}/#{repo}/pulls/#{pull_number}/reviews").
to_return(status: 200, body: pull_request_review_response.to_json,
headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'})
end
# ....
end
The important part of this code is stub_request which takes an HTTP request method, a URL path, and it returns the arguments passed into to_return (as an HTTP response). The response object will include a status, body, and a header.
I used the examples from the WebMock README and the API doc from GitHub to produce the example above (Note: It is untested).
Now, you can define a new test that calls the Mock endpoint:
require 'spec_helper'
class CodeReviewSignedOffTest < ActionDispatch::IntegrationTest
test 'GET pull_request_review_response endpoint from GitHub mock' do
uri = URI('https://api.github.com/repos/octocat/hello-world/pulls/42/reviews')
response = Net::HTTP.get(uri)
data = JSON.parse(response&.body)
...
end
end
Hopefully you'll see the data you defined within your mock API come through. I haven't tested this code, so it might require some debugging.
Let me know if you have any questions or run into trouble writing your tests! Good luck.

Upload image with RSpec has unexpected class on controller

I'm in trouble with RSpec and fixture_file_upload on post request.
Here's the problem: I'm trying to send an image by upload to create a category with image, but when the image arrives, it changes it's class type.
I'm waiting to get in params[:category][:image] an ActionDispatch::Http::UploadedFile but what I receive is ActionController::Parameters.
My request example:
context 'when creates a new main category with valid params' do
let(:category) do
{ category: { name: 'Bolos E bolos',
description: 'São bolos sim',
locale: 'pt-BR',
image: fixture_file_upload('images/test-image.png', 'image/png') } }
end
post '/v4/categories' do
it 'upload image' do
expect { do_request(category) }.to change { ActiveStorage::Blob.count }.from(0).to(1)
end
end
end
what I got:
Failure/Error: expect { do_request(category) }.to change { ActiveStorage::Blob.count }.by(1)
expected `ActiveStorage::Blob.count` to have changed by 1, but was changed by 0
How do I send the image as upload and get it on the controller as ActionDispatch::Http::UploadedFile instead ActionController::Parameters
I could not get your controller spec working, but I managed to get an equivalent request spec working. Having spent 45+ minutes getting no where, I think this is the best I can do. This seems to work. (Just make sure you have an avatar.jpg in the public folder of your rails app.)
## spec/requests/user_spec.rb
require 'rails_helper'
RSpec.describe "Users", type: :request do
describe "it attaches uploaded files" do
it 'attaches the uploaded file' do
file = fixture_file_upload(Rails.root.join('public', 'avatar.jpg'), 'image/jpg')
expect {
post api_users_path, params: { user: {username: "Ben", avatar: file } }
}.to change(ActiveStorage::Attachment, :count).by(1)
end
end
end

Simple POST request failing with em-http-request

The following query works with requestmaker:
URI:
http://www.cleverbot.com/webservicemin/
Query:
start=y&icognoid=wsf&fno=0&sub=Say&islearning=1&cleanslate=false&stimulus=!!!%20there%20was%20an%20error%20!!!&icognocheck=af71393ce00d9126a247df2f53948e79
But it does not work with em-http-request:
require 'eventmachine'
require 'em-http-request'
uri = 'http://www.cleverbot.com/webservicemin/'
query = 'start=y&icognoid=wsf&fno=0&sub=Say&islearning=1&cleanslate=false&stimulus=!!!%20there%20was%20an%20error%20!!!&icognocheck=af71393ce00d9126a247df2f53948e79'
EM.run do
http = EM::HttpRequest.new(uri).post(query: query)
http.callback { puts http.response; EM.stop }
http.errback { puts 'There was an error'; EM.stop }
end
which prints There was an error. I feel stumped because this simple example works with any other method of sending a request and I've checked around to see if my usage was wrong but it doesn't seem to be.
Edit: Just for reference, this is not the correct way to use cleverbot. I made a second mistake by sending the data under :query. If you use http.post(body: query) it will work
Looks like a badly implemented server: it aborts the TCP connection without returning a proper HTTP status code, which is why you see "connection closed by server" when you query http.error.
If you change the default user agent to curl's UA string, you get a response:
http = EM::HttpRequest.new(uri).post({
:query => query,
:head => {'User-Agent' => 'curl/7.30.0'}
})

Ruby stubbing with faraday, can't get it to work

Sorry for the title, I'm too frustrated to come up with anything better right now.
I have a class, Judge, which has a method #stats. This stats method is supposed to send a GET request to an api and get some data as response. I'm trying to test this and stub the stats method so that I don't perform an actual request. This is what my test looks like:
describe Judge do
describe '.stats' do
context 'when success' do
subject { Judge.stats }
it 'returns stats' do
allow(Faraday).to receive(:get).and_return('some data')
expect(subject.status).to eq 200
expect(subject).to be_success
end
end
end
end
This is the class I'm testing:
class Judge
def self.stats
Faraday.get "some-domain-dot-com/stats"
end
end
This currently gives me the error: Faraday does not implement: get
So How do you stub this with faraday? I have seen methods like:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get('http://stats-api.com') { [200, {}, 'Lorem ipsum'] }
end
But I can't seem to apply it the right way. What am I missing here?
Note that Faraday.new returns an instance of Faraday::Connection, not Faraday. So you can try using
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return("some data")
Note that I don't know if returning "some data" as shown in your question is correct, because Faraday::Connection.get should return a response object, which would include the body and status code instead of a string. You might try something like this:
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double("response", status: 200, body: "some data")
)
Here's a rails console that shows the class you get back from Faraday.new
$ rails c
Loading development environment (Rails 4.1.5)
2.1.2 :001 > fara = Faraday.new
=> #<Faraday::Connection:0x0000010abcdd28 #parallel_manager=nil, #headers={"User-Agent"=>"Faraday v0.9.1"}, #params={}, #options=#<Faraday::RequestOptions (empty)>, #ssl=#<Faraday::SSLOptions (empty)>, #default_parallel_manager=nil, #builder=#<Faraday::RackBuilder:0x0000010abcd990 #handlers=[Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]>, #url_prefix=#<URI::HTTP:0x0000010abcd378 URL:http:/>, #proxy=nil>
2.1.2 :002 > fara.class
=> Faraday::Connection
Coming to this late, but incase anyone else is too, this is what worked for me - a combination of the approaches above:
let(:json_data) { File.read Rails.root.join("..", "fixtures", "ror", "501100000267.json") }
before do
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double(Faraday::Response, status: 200, body: json_data, success?: true)
)
end
Faraday the class has no get method, only the instance does. Since you are using this in a class method what you can do is something like this:
class Judge
def self.stats
connection.get "some-domain-dot-com/stats"
end
def self.connection=(val)
#connection = val
end
def self.connection
#connection ||= Faraday.new(some stuff to build up connection)
end
end
Then in your test you can just set up a double:
let(:connection) { double :connection, get: nil }
before do
allow(connection).to receive(:get).with("some-domain-dot-com/stats").and_return('some data')
Judge.connection = connection
end
I ran into the same problem with Faraday::Adapter::Test::Stubs erroring with Faraday does not implement: get. It seems you need to set stubs to a Faraday adapter, like so:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get("some-domain-dot-com/stats") { |env| [200, {}, 'egg'] }
end
test = Faraday.new do |builder|
builder.adapter :test, stubs
end
allow(Faraday).to receive(:new).and_return(test)
expect(Judge.stats.body).to eq "egg"
expect(Judge.stats.status).to eq 200
A better way to do this, rather than using allow_any_instance_of, is to set the default connection for Faraday, so that Faraday.get will use the connection you setup in your tests.
For example:
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } }
before do
stubs.get('/maps/api/place/details/json') do |_env|
[
200,
{ 'Content-Type': 'application/json' },
{ 'result' => { 'photos' => [] } }.to_json
]
end
Faraday.default_connection = conn
end
after do
Faraday.default_connection = nil
end

Rails pagination in API using Her, Faraday

I've been trying to figure this out all day, and it's driving me crazy.
I have two rails apps, ServerApp and ClientApp. ClientApp gets data from ServerApp through an API, using the Her gem. Everything was great until I needed pagination information.
This is the method I am using to get the orders (this uses kamainari for pagination and ransack for search):
# ServerApp
def search
#search = Order.includes(:documents, :client).order('id desc').search(params[:q])
#orders = #search.result(distinct: true).page(params[:page]).per(params[:per])
respond_with #orders.as_json(include: :documents)
end
It returns an array of hashes in json, which Her uses as a collection of orders. That works fine.
# Response
[
{
"client_id": 239,
"created_at": "2013-05-15T15:37:03-07:00",
"id": 2422,
"ordered_at": "2013-05-15T15:37:03-07:00",
"origin": "online",
"updated_at": "2013-05-15T15:37:03-07:00",
"documents": [
{ ... }
]
},
...
]
But I needed pagination information. It looked like I needed to send it as metadata with my json. So I change my response to this:
respond_to do |format|
format.json do
render json: { orders: #orders.as_json(include: :documents), metadata: 'sent' }
end
end
This does indeed send over metadata, so in my ClientApp I can write #orders.metadata and get 'sent'. But now my orders are nested in an array inside of 'orders', so I need to use #orders.orders, and then it treats it like an array instead of a Her collection.
After doing some reading, it seemed sending pagination info through headers was the way a lot of other people did this (I was able to get the headers set up in an after_filter using this guide). But I am even more lost on how to get those response headers in my ClientApp - I believe I need a Faraday Middleware but I just am having no luck getting this to work.
If anyone knows how I can just get this done, I would be very grateful. I can't take another day of banging my head against the wall on this, but I feel like I am just one vital piece of info away from solving this!
I encountered the same issue and solved it by adding my own middleware and rewriting the "parse" and "on_complete" methods without that much hassle and avoiding the use of global variables.
Here's the code:
class CustomParserMiddleware < Her::Middleware::DefaultParseJSON
def parse(env)
json = parse_json(env[:body])
pagination = parse_json(env[:response_headers][:pagination_key]) || {}
errors = json.delete(:errors) || {}
metadata = json.delete(:metadata) || {}
{
:data => json,
:errors => errors,
:metadata => {
:pagination => pagination,
:additional_metadata => metadata
},
end
def on_complete(env)
env[:body] = case env[:status]
when 204
parse('{}')
else
parse(env)
end
end
end
then, you can access the pagination as follows:
model = Model.all
model.metadata[:pagination]
I finally got this working. The trick was to use a global variable in the faraday on_complete - I tried to find a better solution but this was the best I could do. Once again, I got the header code from here. Here's the full guide to how to get pagination working with Her:
First, on my server side, I have the Kaminari gem, and I pass page and per as params to the server from the client. (This is also using ransack for searching)
def search
#search = Order.order('id desc').search(params[:q])
#orders = #search.result(distinct: true).page(params[:page]).per(params[:per])
respond_with #orders.as_json(include: :items)
end
My client makes the request like so:
#orders = Order.search(q: { client_id_eq: #current_user.id }, page: params[:page], per: 3)`
Back on the server, I have this in my ApiController (app controller for api):
protected
def self.set_pagination_headers(name, options = {})
after_filter(options) do |controller|
results = instance_variable_get("##{name}")
headers["X-Pagination"] = {
total_count: results.total_count,
offset_value: results.offset_value
}.to_json
end
end
In the server orders_controller.rb, I set the pagination headers for the search method:
class OrdersController < ApiController
set_pagination_headers :orders, only: [:search]
...
end
Now to receive the headers we need a Faraday middleware in Her on the client.
# config/initializers/her.rb
Her::API.setup url: Constants.api.url do |c|
c.use TokenAuthentication
c.use HeaderParser # <= This is my middleware for headers
c.use Faraday::Request::UrlEncoded
c.use Her::Middleware::DefaultParseJSON
c.use Faraday::Adapter::NetHttp
c.use Faraday::Response::RaiseError
end
# lib/header_parser.rb
# don't forget to load this file in application.rb with something like:
# config.autoload_paths += Dir[File.join(Rails.root, "lib", "*.rb")].each { |l| require l }
class HeaderParser < Faraday::Response::Middleware
def on_complete(env)
unless env[:response_headers]['x-pagination'].nil?
# Set the global var for pagination
$pagination = JSON.parse(env[:response_headers]['x-pagination'], symbolize_names: true)
end
end
end
Now back in your client controller, you have a global variable of hash called $pagination; mine looks like this:
$pagintation = { total_count: 0, offset_value: 0 }`
Finally, I added Kaminari gem to my client app to paginate the array and get those easy pagination links:
#orders = Kaminari.paginate_array(#orders, total_count: $pagination[:total_count]).page(params[:page]).per(params[:per_page])`
I hope this can help someone else, and if anyone knows a better way to do this, let me know!
You can pass header options to Faraday when setting up the connection, see the docs at http://rubydoc.info/gems/faraday/0.8.7/Faraday/Connection:initialize
Sometimes it helps to do a curl request first, esp. use -vv option for verbose output where you will see all headers. (Maybe you can attach some log outputs from the Server too)
You can use e.g. clogger (http://clogger.rubyforge.org/) do monitor header information on the Rails server side

Resources