How do I test my JSON API with Sinatra + rspec - api

I have a post method that accepts JSON:
post '/channel/create' do
content_type :json
#data = JSON.parse(env['rack.input'].gets)
if #data.nil? or !#data.has_key?('api_key')
status 400
body({ :error => "JSON corrupt" }.to_json)
else
status 200
body({ :error => "Channel created" }.to_json)
end
As a newbie to rspec I am bewildered trying to figure out how to write a test against that POST with an acceptable JSON payload. The closest I got to is this which is woefully inaccurate but I don't seem to be asking the Google god the right questions to help me out here.
it "accepts create channel" do
h = {'Content-Type' => 'application/json'}
body = { :key => "abcdef" }.to_json
post '/channel/create', body, h
last_response.should be_ok
end
Any best practice guidance for testing APIs in Sinatra will be most appreciated also.

The code you've used is fine, although I would structure it slightly differently as I don't like to use it blocks the way you normally see them, I think it encourages testing of more than one aspect of a system at a time:
let(:body) { { :key => "abcdef" }.to_json }
before do
post '/channel/create', body, {'CONTENT_TYPE' => 'application/json'}
end
subject { last_response }
it { should be_ok }
I've used let because it's better than an instance variable in a before block (kudos to you for not doing that). The post is in a before block because it's not really part of the spec, but a side effect that occurs prior to what you're speccing. The subject is the response and that makes the it a simple call.
Because checking the response is ok is needed so often I put it in a shared example:
shared_examples_for "Any route" do
subject { last_response }
it { should be_ok }
end
and then call it as such:
describe "Creating a new channel" do
let(:body) { { :key => "abcdef" }.to_json }
before do
post '/channel/create', body, {'CONTENT_TYPE' => 'application/json'}
end
it_should_behave_like "Any route"
# now spec some other, more complicated stuff…
subject { JSON.parse(last_response.body) }
it { should == "" }
and because the content type changes so often, I put that in a helper:
module Helpers
def env( *methods )
methods.each_with_object({}) do |meth, obj|
obj.merge! __send__(meth)
end
end
def accepts_html
{"HTTP_ACCEPT" => "text/html" }
end
def accepts_json
{"HTTP_ACCEPT" => "application/json" }
end
def via_xhr
{"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
end
It's easy to add this in where it's needed by including it via the RSpec config:
RSpec.configure do |config|
config.include Helpers, :type => :request
then:
describe "Creating a new channel", :type => :request do
let(:body) { { :key => "abcdef" }.to_json }
before do
post '/channel/create', body, env(:accepts_json)
end
Having said all that, personally, I wouldn't post using JSON. HTTP POST is simple to handle, and every form and javascript library does it easily and well. Respond with JSON by all means, but don't post JSON, HTTP is a lot easier.
Edit: after writing out the Helpers bit above I realised it would be more helpful as a gem.

Looks like the ability to do post :update, '{"some": "json"}' was added to the internal ActionPack test_case.rb used by rspec in this commit:
https://github.com/rails/rails/commit/5b9708840f4cc1d5414c64be43c5fc6b51d4ecbf
Since you're using Sinatra I'm not sure the best way to get those changes—you might be able to upgrade ActionPack directly, or patch from the above commit.

If you want to look at last_response as JSON, you could try rack-test-json which makes this trivial:
expect(last_response).to be_json
expect(last_response.as_json['key']).to be == 'value'

Related

Can't create a user factory in factory girl when using rspec, devise, guard, and spork

I've seen this issue several places, but none of the solutions seem to work.
I have a Rails 3.1 app with the latest versions of guard, spork, factory girl, rspec, and devise.
Whenever I try to create a user factory (the user model is a devise model) then I get this error:
Could not find a valid mapping for #<User...model attributes...>
I'm not sure what the problem is.
I ran rake db:test:prepare. I followed the instructions in this stackoverflow question: "Could not find a valid mapping for #<User ...>" only on second and successive tests
ALso, I attempted the solution in this answer from google groups:
https://groups.google.com/forum/?fromgroups#!topic/plataformatec-devise/StpbEsDCec0[1-25]
And, here's all the relevant code:
Guardfile
# A sample Guardfile
# More info at https://github.com/guard/guard#readme
require 'capybara/rspec'
guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do
watch('config/application.rb')
watch('config/environment.rb')
watch('config/environments/test.rb')
watch(%r{^config/initializers/.+\.rb$})
watch('Gemfile')
watch('Gemfile.lock')
watch('spec/spec_helper.rb') { :rspec }
watch('test/test_helper.rb') { :test_unit }
watch(%r{features/support/}) { :cucumber }
end
guard 'rspec', :version => 2, :cli => '--drb' do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
# Rails example
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
watch('config/routes.rb') { "spec/routing" }
watch('app/controllers/application_controller.rb') { "spec/controllers" }
# Capybara request specs
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# Turnip features and steps
watch(%r{^spec/acceptance/(.+)\.feature$})
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
end
</code>
This is in my spec/factories.rb
FactoryGirl.define do
load "#{Rails.root}/app/models/user.rb"
factory :user, class: User do |user|
email 'owner#example.com'
password '12345678'
password_confirmation '12345678'
companyid 'example_company'
end
end
This is my spec/controllers/api_controller_spec.rb
require 'spec_helper'
describe ApiController do
it 'verifies company_id through POST to api/company_id' do
load "#{Rails.root}/app/models/user.rb"
debugger
user = FactoryGirl.create(:user)
post(:get_company_id, {:company_id => 'example_company'})
response.body.should include('true')
end
end
And I have this at the end of my config/application.rb
ActionDispatch::Callbacks.after do
# Reload the factories
return unless (Rails.env.development? || Rails.env.test?)
unless FactoryGirl.factories.blank? # first init will load factories, this should only run on subsequent reloads
FactoryGirl.factories.clear
FactoryGirl.find_definitions
end
end
I'm really desperate for an answer here because otherwise I won't be able to test my User model (which is the most important model I have).
Feel free to comment and ask any questions.
EDIT: code looked funny in places, so I edited it for clarity
UPDATE:
So I tried simplifying everything to get to the core of the problem, and I'm pretty sure that devise and factory girl don't "like" each other. I'm still getting the exact same error whenever I try and create a user factory.
This is my new setup (I reverted to a previous git commit and I no longer have guard or spork).
My factories.rb is exactly the same as Michael Durant's except I have an extra line:
companyid 'example'
That's just a requirement for my app.
My spec_helper.rb requires rubygems and capybara/rspec and that's it.
And this is my spec/models/user_spec.rb
require 'spec_helper'
describe 'User associations' do
it 'tests creation of user' do
debugger
user = FactoryGirl.create(:user)
User.count.should be(1)
end
end
Also, this is interesting: When I hit that debugger statement and type in
eval User
It shows the mapping of a valid User.
UPDATE:
So, it's not factory girl that's the problem. It's devise.
This is the new api_controller_spec.rb file and it comes up with the same error of not having a valid mapping of the user.
require 'spec_helper'
describe ApiController do
it 'verifies company_id through POST to api/company_id' do
load "#{Rails.root}/app/models/user.rb"
debugger
user = User.new
user.email = 'owner#example.com'
user.password = '12345678'
user.password_confirmation = '12345678'
user.company_id = 'company'
user.save
post(:get_company_id, {:company_id => 'example_company'})
response.body.should include('true')
end
end
THere isn't a problem with any other environment as I can create users fine through the console, while a local server is running, or when the code is pushed up to Heroku. It might be rspec or something else, but I'm just not sure at this point.
I would recommend you simplify things to find the issue. Currently I feel you have too much going on / too many variable factors.
I would recommend the following:
1 Make a new branch. I assume you are using git, if not use it (git init) and make a fork.
2 Remove all the spork and guard stuff. They are helpful in speeding up your tests and running tests in a CI (Continuous Integration), but they are certainly not 'needed' and removing them will help uncover what the real problems are.
3 Set up your user factory correctly. We use this:
FactoryGirl.define do
sequence :email do |n|
"email#{n}#factory.com"
end
factory :user do
email
first_name { 'First' }
last_name { 'Last' }
password { "password" }
password_confirmation { "password" }
association :area
role { 'super_user' }
end
end
4 Set up your spec_help correctly.
We use these requires in our spec_helper.rb:
require 'rubygems'
require 'capybara/rspec'
5 Try to get one user test to pass using spec/models/user_spec.rb, something like:
require 'spec_helper'
describe 'User associations' do
subject { User.new }
it { should validate_presence_of :area }
...
So, the answer had nothing to do with guard, spork, rspec, or factory_girl.
The problem was that I had my devise_for :users routes commented out since I've been doing a huge overhaul of my rails app.
It's always something stupidly simple >.<

No request params in constraint when testing route

When I run this in practice it works, but I can't seem to write a working test for my route constraint with rspec.
When the test runs the constraint is triggered, but the request params are empty, thus it does not validate and the test fails.
I am running Rails 3.0.9, rspec-rails 2.6.1 and rspec 2.6.0.
config/routes.rb
match ":param1-unique-:param2" => "controller#index",
:constraints => ParamConstraint.new
lib/param_constraint.rb
class ParamConstraint
def matches?(request)
#request ||= request
valid_param1? && valid_param2?
end
def valid_param1?
#request.params[:param1] == "lorem"
end
def valid_param2?
#request.params[:param2] == "ipsum"
end
end
spec/routing/param_constraint_spec.rb
require 'spec_helper'
describe "param constraint routing" do
it "recognizes route for param1 and param2" do
{ :get => "/lorem-unique-ipsum" }.
should route_to(
:controller => "controller",
:action => "index",
:param1 => "lorem",
:param2 => "ipsum"
)
end
end
Update
If I inspect the request in the constraint I get the following output:
#<ActionDispatch::Request:0x007fee140ff910 #env={
"rack.version"=>[1, 1],
"rack.input"=>#<StringIO:0x007fee1446da48>,
"rack.errors"=>#<StringIO:0x007fee1446e768>,
"rack.multithread"=>true,
"rack.multiprocess"=>true,
"rack.run_once"=>false,
"REQUEST_METHOD"=>"GET",
"SERVER_NAME"=>"example.org",
"SERVER_PORT"=>"80",
"QUERY_STRING"=>"",
"PATH_INFO"=>"/lorem-unique-ipsum",
"rack.url_scheme"=>"http",
"HTTPS"=>"off",
"SCRIPT_NAME"=>"",
"CONTENT_LENGTH"=>"0"
}>
I ran into this same issue today, searching for an answer brought me to this page's question. For what it's worth, I had to resort to writing a request spec instead.
context "passing params that satisfy ParamConstraint" do
before do
visit "/lorem-unique-ipsum"
end
it "should serve up a page with content" do
# replace this with some assertion that gets satisfied by
# pages served up when ParamConstraint.new.matches? returns true
page.should have_selector("html body div#foo")
page.should_not have_selector("html body div#bar")
end
end
context "passing params that DO NOT satisfy ParamConstraint" do
before do
visit "/other-unique-other"
end
it "should serve up a page with different content" do
# replace this with some assertion that gets satisfied by
# pages served up when ParamConstraint.new.matches? returns false
page.should_not have_selector("html body div#foo")
page.should have_selector("html body div#bar")
end
end
This doesn't answer your question, which I take to be "how to test routing constraint", as the proper way would be via a routing spec. But given this gap in how request.params works when you use "should route_to", this is a workaround. A request spec, as opposed to a routing spec, will fill request.params correctly.
Same issue exists years later, with rspec-core 3.4.4, rspec-rails 3.4.2, rails 4.2.6. Don't have time to dig into exactly why...
You can use a request spec as suggested above, but don't use it to test the page contents. Instead, replicate a routing test (route_to) by checking the conversion of URL paths to request params:
RSpec.describe 'routes', type: :request do
describe '/:slug' do
it 'routes correctly' do
get '/test-product-slug'
expect(request.params).to eq(
'controller' => 'product',
'action' => :index,
'slug' => 'test-product-slug'
)
end
end
end

Paperclip- validate pdfs with content_type='application/octet-stream'

I was using paperclip for file upload. with validations as below:
validates_attachment_content_type :upload, :content_type=>['application/pdf'],
:if => Proc.new { |module_file| !module_file.upload_file_name.blank? },
:message => "must be in '.pdf' format"
But, my client complained today that he is not able to upload pdf. After investigating I come to know from request headers is that the file being submitted had content_type=application/octet-stream.
Allowing application/octet-stream will allow many type of files for upload.
Please suggest a solution to deal with this.
Seems like paperclip doesn't detect content type correctly. Here is how I was able to fix it using custom content-type detection and validation (code in model):
VALID_CONTENT_TYPES = ["application/zip", "application/x-zip", "application/x-zip-compressed", "application/pdf", "application/x-pdf"]
before_validation(:on => :create) do |file|
if file.media_content_type == 'application/octet-stream'
mime_type = MIME::Types.type_for(file.media_file_name)
file.media_content_type = mime_type.first.content_type if mime_type.first
end
end
validate :attachment_content_type
def attachment_content_type
errors.add(:media, "type is not allowed") unless VALID_CONTENT_TYPES.include?(self.media_content_type)
end
Based on the above, here's what I ended up with which is compatible with PaperClip 4.2 and Rails 4:
before_post_process on: :create do
if media_content_type == 'application/octet-stream'
mime_type = MIME::Types.type_for(media_file_name)
self.media_content_type = mime_type.first.to_s if mime_type.first
end
end
For paperclip 3.3 and Rails 3, I did this a bit differently
before_validation on: :create do
if media_content_type == 'application/octet-stream'
mime_type = MIME::Types.type_for(media_file_name)
self.media_content_type = mime_type.first if mime_type.first
end
end
validates_attachment :media, content_type: { content_type: VALID_CONTENT_TYPES }
By the way, i needed to do this because testing with Capybara and phantom js using attach_file did not generate the correct mime type for some files.

How do I write a Rails 3.1 engine controller test in rspec?

I have written a Rails 3.1 engine with the namespace Posts. Hence, my controllers are found in app/controllers/posts/, my models in app/models/posts, etc. I can test the models just fine. The spec for one model looks like...
module Posts
describe Post do
describe 'Associations' do
it ...
end
... and everything works fine.
However, the specs for the controllers do not work. The Rails engine is mounted at /posts, yet the controller is Posts::PostController. Thus, the tests look for the controller route to be posts/posts.
describe "GET index" do
it "assigns all posts as #posts" do
Posts::Post.stub(:all) { [mock_post] }
get :index
assigns(:posts).should eq([mock_post])
end
end
which yields...
1) Posts::PostsController GET index assigns all posts as #posts
Failure/Error: get :index
ActionController::RoutingError:
No route matches {:controller=>"posts/posts"}
# ./spec/controllers/posts/posts_controller_spec.rb:16
I've tried all sorts of tricks in the test app's routes file... :namespace, etc, to no avail.
How do I make this work? It seems like it won't, since the engine puts the controller at /posts, yet the namespacing puts the controller at /posts/posts for the purpose of testing.
I'm assuming you're testing your engine with a dummy rails app, like the one that would be generated by enginex.
Your engine should be mounted in the dummy app:
In spec/dummy/config/routes.rb:
Dummy::Application.routes.draw do
mount Posts::Engine => '/posts-prefix'
end
My second assumption is that your engine is isolated:
In lib/posts.rb:
module Posts
class Engine < Rails::Engine
isolate_namespace Posts
end
end
I don't know if these two assumptions are really required, but that is how my own engine is structured.
The workaround is quite simple, instead of this
get :show, :id => 1
use this
get :show, {:id => 1, :use_route => :posts}
The :posts symbol should be the name of your engine and NOT the path where it is mounted.
This works because the get method parameters are passed straight to ActionDispatch::Routing::RouteSet::Generator#initialize (defined here), which in turn uses #named_route to get the correct route from Rack::Mount::RouteSet#generate (see here and here).
Plunging into the rails internals is fun, but quite time consuming, I would not do this every day ;-) .
HTH
I worked around this issue by overriding the get, post, put, and delete methods that are provided, making it so they always pass use_route as a parameter.
I used Benoit's answer as a basis for this. Thanks buddy!
module ControllerHacks
def get(action, parameters = nil, session = nil, flash = nil)
process_action(action, parameters, session, flash, "GET")
end
# Executes a request simulating POST HTTP method and set/volley the response
def post(action, parameters = nil, session = nil, flash = nil)
process_action(action, parameters, session, flash, "POST")
end
# Executes a request simulating PUT HTTP method and set/volley the response
def put(action, parameters = nil, session = nil, flash = nil)
process_action(action, parameters, session, flash, "PUT")
end
# Executes a request simulating DELETE HTTP method and set/volley the response
def delete(action, parameters = nil, session = nil, flash = nil)
process_action(action, parameters, session, flash, "DELETE")
end
private
def process_action(action, parameters = nil, session = nil, flash = nil, method = "GET")
parameters ||= {}
process(action, parameters.merge!(:use_route => :my_engine), session, flash, method)
end
end
RSpec.configure do |c|
c.include ControllerHacks, :type => :controller
end
Use the rspec-rails routes directive:
describe MyEngine::WidgetsController do
routes { MyEngine::Engine.routes }
# Specs can use the engine's routes & named URL helpers
# without any other special code.
end
– RSpec Rails 2.14 official docs.
Based on this answer I chose the following solution:
#spec/spec_helper.rb
RSpec.configure do |config|
# other code
config.before(:each) { #routes = UserManager::Engine.routes }
end
The additional benefit is, that you don't need to have the before(:each) block in every controller-spec.
Solution for a problem when you don't have or cannot use isolate_namespace:
module Posts
class Engine < Rails::Engine
end
end
In controller specs, to fix routes:
get :show, {:id => 1, :use_route => :posts_engine}
Rails adds _engine to your app routes if you don't use isolate_namespace.
I'm developing a gem for my company that provides an API for the applications we're running. We're using Rails 3.0.9 still, with latest Rspec-Rails (2.10.1). I was having a similar issue where I had defined routes like so in my Rails engine gem.
match '/companyname/api_name' => 'CompanyName/ApiName/ControllerName#apimethod'
I was getting an error like
ActionController::RoutingError:
No route matches {:controller=>"company_name/api_name/controller_name", :action=>"apimethod"}
It turns out I just needed to redefine my route in underscore case so that RSpec could match it.
match '/companyname/api_name' => 'company_name/api_name/controller_name#apimethod'
I guess Rspec controller tests use a reverse lookup based on underscore case, whereas Rails will setup and interpret the route if you define it in camelcase or underscore case.
It was already mentioned about adding routes { MyEngine::Engine.routes }, although it's possible to specify this for all controller tests:
# spec/support/test_helpers/controller_routes.rb
module TestHelpers
module ControllerRoutes
extend ActiveSupport::Concern
included do
routes { MyEngine::Engine.routes }
end
end
end
and use in rails_helper.rb:
RSpec.configure do |config|
config.include TestHelpers::ControllerRoutes, type: :controller
end

Why do I get a connection timeout for facebook api-video?

This is the API call Im attempting:
http://developers.facebook.com/docs/reference/rest/video.upload
(Video upload is not available in the new Graph API.)
I have tried many variations on the parameters. The code below is my best guess. If I modify the params to be obviously incorrect, change to http (not https) or try to use api.facebook.com for video, I get proper errors back.
However, my code below just waits a few minutes before reporting:
ETIMEDOUT: Connection timed out
Also included is working code to upload a photo - which is almost identical.
Ruby:
# Facebook Old-API method - testing only - this works.
def post_photo
url = "https://api.facebook.com/method/photos.upload"
body = {
nil => File.new(self.media.media_files.first.source_file, 'rb'),
:access_token => self.session.auth_data[:access_token],
:callback => "none",
:aid => "Test Photos",
:caption => "Test",
:uid => self.session.auth_data[:uid],
}
response = RestClient.post url, body
end
# Facebook Old-API method - doesn't work - connection timeout.
def post_video
url = "https://api-video.facebook.com/method/video.upload"
body = {
:nil => File.new(self.media.media_files.first.source_file, 'rb'),
:access_token => self.session.auth_data[:access_token],
:callback => "none",
:title => "Test title",
:description => "Test description",
:privacy => "{ value: 'EVERYONE' }",
:uid => self.session.auth_data[:uid],
}
response = RestClient.post url, body
end
PS: Im in Australia - is the API limited to eg the USA?
Thanks