Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSpec integration #60

Merged
merged 5 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions lib/rage/rspec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# frozen_string_literal: true

require "rack/test"
require "json"

# set up environment
ENV["RAGE_ENV"] ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test"

# load the app
require "bundler/setup"
require "rage"
require_relative "#{Rage.root}/config/application"

# verify the environment
abort("The test suite is running in #{Rage.env} mode instead of 'test'!") unless Rage.env.test?

# mock fiber methods as RSpec tests don't run concurrently
class Fiber
def self.schedule(&block)
fiber = Fiber.new(blocking: true) do
Fiber.current.__set_result(block.call)
end
fiber.resume

fiber
end

def self.await(_)
# no-op
end
end

# define request helpers
module RageRequestHelpers
include Rack::Test::Methods

alias_method :response, :last_response

APP = Rack::Builder.parse_file("#{Rage.root}/config.ru").yield_self do |app|
app.is_a?(Array) ? app[0] : app
end

def app
APP
end

%w(get options head).each do |method_name|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{method_name}(path, params: {}, headers: {})
request("#{method_name.upcase}", path, params: params, headers: headers)
end
RUBY
end

%w(post put patch delete).each do |method_name|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{method_name}(path, params: {}, headers: {}, as: nil)
if as == :json
params = params.to_json
headers["content-type"] = "application/json"
end

request("#{method_name.upcase}", path, params: params, headers: headers.merge("IODINE_HAS_BODY" => !params.empty?))
end
RUBY
end

def request(method, path, params: {}, headers: {})
if headers.any?
headers = headers.transform_keys do |k|
if k.downcase == "content-type"
"CONTENT_TYPE"
elsif k.downcase == "content-length"
"CONTENT_LENGTH"
elsif k.upcase == k
k
else
"HTTP_#{k.tr("-", "_").upcase! || k}"
end
end
end

custom_request(method, path, params, headers)
end

def host!(host)
@__host = host
end

def default_host
@__host || "example.org"
end
end

# include request helpers
RSpec.configure do |config|
config.include(RageRequestHelpers, type: :request)
end

# patch MockResponse class
class Rack::MockResponse
def parsed_body
if headers["content-type"].start_with?("application/json")
JSON.parse(body)
else
body
end
end

def code
status.to_s
end

alias_method :response_code, :status
end

# define http status matcher
RSpec::Matchers.matcher :have_http_status do |expected|
codes = Rack::Utils::SYMBOL_TO_STATUS_CODE

failure_message do |response|
actual = response.status

if expected.is_a?(Integer)
"expected the response to have status code #{expected} but it was #{actual}"
elsif expected == :success
"expected the response to have a success status code (2xx) but it was #{actual}"
elsif expected == :error
"expected the response to have an error status code (5xx) but it was #{actual}"
elsif expected == :missing
"expected the response to have a missing status code (404) but it was #{actual}"
else
"expected the response to have status code :#{expected} (#{codes[expected]}) but it was :#{codes.key(actual)} (#{actual})"
end
end

failure_message_when_negated do |response|
actual = response.status

if expected.is_a?(Integer)
"expected the response not to have status code #{expected} but it was #{actual}"
elsif expected == :success
"expected the response not to have a success status code (2xx) but it was #{actual}"
elsif expected == :error
"expected the response not to have an error status code (5xx) but it was #{actual}"
elsif expected == :missing
"expected the response not to have a missing status code (404) but it was #{actual}"
else
"expected the response not to have status code :#{expected} (#{codes[expected]}) but it was :#{codes.key(actual)} (#{actual})"
end
end

match do |response|
actual = response.status

case expected
when :success
actual >= 200 && actual < 300
when :error
actual >= 500
when :missing
actual == 404
when Symbol
actual == codes.fetch(expected)
else
actual == expected
end
end
end

if defined? RSpec::Rails::Matchers
module RSpec::Rails::Matchers
def have_http_status(_)
super
end
end
end
1 change: 1 addition & 0 deletions rage.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
spec.add_dependency "rack", "~> 2.0"
spec.add_dependency "rage-iodine", "~> 3.0"
spec.add_dependency "zeitwerk", "~> 2.6"
spec.add_dependency "rack-test", "~> 2.1"
end
2 changes: 2 additions & 0 deletions spec/rspec/config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
run Rage.application
Rage.load_middlewares(self)
1 change: 1 addition & 0 deletions spec/rspec/config/application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Rage.configure {}
134 changes: 134 additions & 0 deletions spec/rspec/rspec_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
module RspecHelpersSpec
class TestController < RageController::API
def index
render json: %w(test_1 test_2 test_3)
end

def params_action
render plain: params[:i]
end

def headers_action
if request.headers["Content-Type"] == "text/plain" &&
request.headers["Cache-Control"] == "max-age=604800, must-revalidate" &&
request.headers["Last-Modified"] == "Sun, 03 Sep 2017 00:17:02 GMT"
head :ok
else
head :bad_request
end
end

def fibers_action
i = 0

Fiber.await([
Fiber.schedule { i += 1 },
Fiber.schedule { i += 2 },
])

render plain: i
end

def subdomain
head :ok
end
end

Rage.routes.draw do
root to: "rspec_helpers_spec/test#index"

get "params", to: "rspec_helpers_spec/test#params_action"
post "params", to: "rspec_helpers_spec/test#params_action"

get "headers", to: "rspec_helpers_spec/test#headers_action"
get "fibers", to: "rspec_helpers_spec/test#fibers_action"

get "subdomain", to: "rspec_helpers_spec/test#subdomain", constraints: { host: /rage-test/ }
end
end

RSpec.describe "RSpec helpers", type: :request do
before do
allow(Rage).to receive(:root).and_return(Pathname.new(__dir__).expand_path)
require "rage/rspec"
end

it "correctly parses responses" do
get "/"

expect(response.body).to eq('["test_1","test_2","test_3"]')
expect(response.parsed_body).to eq(%w(test_1 test_2 test_3))
end

it "correctly matches http statuses" do
get "/"

expect(response).to have_http_status(:ok)
expect(response).to have_http_status(200)

expect {
expect(response).to have_http_status(:missing)
}.to raise_error("expected the response to have a missing status code (404) but it was 200")

expect {
expect(response).to have_http_status(500)
}.to raise_error("expected the response to have status code 500 but it was 200")

expect {
expect(response).not_to have_http_status(:success)
}.to raise_error("expected the response not to have a success status code (2xx) but it was 200")
end

it "correctly passes params" do
get "/params?i=222"
expect(response.body).to eq("222")

post "/params?i=333"
expect(response.body).to eq("333")

post "/params", params: { i: "444" }
expect(response.body).to eq("444")

post "/params", params: { i: "555" }, as: :json
expect(response.body).to eq("555")
end

it "works correctly with no params" do
post "/params"
expect(response.body).to eq("")
end

it "correctly passes headers" do
get "/headers", headers: {
"content-type" => "text/plain",
"Cache-Control" => "max-age=604800, must-revalidate",
"HTTP_LAST_MODIFIED" => "Sun, 03 Sep 2017 00:17:02 GMT"
}

expect(response).to have_http_status(:ok)
end

it "allows to correctly schedule fibers" do
get "/fibers"
expect(response.body).to eq("3")
end

it "uses the default host value" do
get "/subdomain"
expect(response).to have_http_status(:not_found)
end

it "allows to modify host value" do
host! "rage-test.com"
get "/subdomain"

expect(response).to have_http_status(:ok)
end

it "allows to stub app calls" do
allow_any_instance_of(RageController::API).to receive(:params).and_return({ i: "12345" })

get "/params?i=111"
expect(response.body).to eq("12345")
end
end
Loading