spokes 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +12 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +2 -0
- data/README.md +137 -0
- data/Rakefile +1 -0
- data/lib/spokes.rb +13 -0
- data/lib/spokes/config/env.rb +65 -0
- data/lib/spokes/middleware/concerns/bad_request.rb +22 -0
- data/lib/spokes/middleware/concerns/header_validation.rb +23 -0
- data/lib/spokes/middleware/cors.rb +71 -0
- data/lib/spokes/middleware/health.rb +79 -0
- data/lib/spokes/middleware/request_id.rb +43 -0
- data/lib/spokes/middleware/service_name.rb +65 -0
- data/lib/spokes/version.rb +3 -0
- data/lib/spokes/versioning/config/minor_versions.yml +3 -0
- data/lib/spokes/versioning/minor_versioning.rb +71 -0
- data/lib/spokes/versioning/railtie.rb +22 -0
- data/lib/spokes/versioning/tasks/minor_versioning.rake +14 -0
- data/logo/spokes_logo.png +0 -0
- data/script/cibuild +12 -0
- data/script/postbuild +17 -0
- data/script/test +23 -0
- data/spec/config/env_spec.rb +111 -0
- data/spec/middleware/cors_spec.rb +29 -0
- data/spec/middleware/health_spec.rb +107 -0
- data/spec/middleware/request_id_spec.rb +19 -0
- data/spec/middleware/service_name_spec.rb +60 -0
- data/spec/rails_helper.rb +3 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/versioning/minor_versioning_spec.rb +45 -0
- data/spokes.gemspec +29 -0
- metadata +255 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Spokes
|
2
|
+
module Middleware
|
3
|
+
class RequestID
|
4
|
+
PATTERN = /^[\w\\-_\\.\d]+$/
|
5
|
+
|
6
|
+
def initialize(app, service_name:)
|
7
|
+
raise "invalid name: #{service_name}" unless service_name =~ PATTERN
|
8
|
+
|
9
|
+
@app = app
|
10
|
+
@service_name = service_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
id = env['action_dispatch.request_id'] || SecureRandom.uuid
|
15
|
+
request_ids = extract_request_ids(env).insert(0, @service_name + ':' + id)
|
16
|
+
|
17
|
+
# make ID of the request accessible to consumers down the stack
|
18
|
+
env['REQUEST_ID'] = request_ids[0]
|
19
|
+
|
20
|
+
# Extract request IDs from incoming headers as well. Can be used for
|
21
|
+
# identifying a request across a number of components in SOA.
|
22
|
+
env['REQUEST_IDS'] = request_ids
|
23
|
+
|
24
|
+
Thread.current[:request_chain] = env['REQUEST_IDS']
|
25
|
+
|
26
|
+
@app.call(env)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def extract_request_ids(env)
|
32
|
+
request_ids = raw_request_ids(env)
|
33
|
+
request_ids.map!(&:strip)
|
34
|
+
request_ids
|
35
|
+
end
|
36
|
+
|
37
|
+
def raw_request_ids(_env)
|
38
|
+
%w[HTTP_REQUEST_CHAIN].each_with_object([]) do |key, _request_ids|
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Spokes
|
2
|
+
module Middleware
|
3
|
+
# Validates inbound and sets outbound Service-Name HTTP headers.
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
#
|
7
|
+
# class Application < Rails::Application
|
8
|
+
# config.middleware.use Spokes::Middleware::ServiceName
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
class ServiceName
|
12
|
+
include Middleware::Concerns::BadRequest
|
13
|
+
include Middleware::Concerns::HeaderValidation
|
14
|
+
|
15
|
+
PATTERN = /^[\w\\-_\\.\d]+$/
|
16
|
+
HEADER_NAME = 'Service-Name'.freeze
|
17
|
+
|
18
|
+
def initialize(app, service_name:, exclude_paths: [])
|
19
|
+
raise "invalid name: #{service_name}" unless service_name =~ PATTERN
|
20
|
+
@app = app
|
21
|
+
@service_name = service_name
|
22
|
+
@exclude_paths = path_to_regex(exclude_paths)
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
begin
|
27
|
+
unless exclude?(env['PATH_INFO'].chomp('/'))
|
28
|
+
validate_header_presence(env: env, header_name: HEADER_NAME)
|
29
|
+
validate_header_pattern(env: env, header_name: HEADER_NAME, pattern: PATTERN)
|
30
|
+
end
|
31
|
+
rescue Middleware::Concerns::HeaderValidation::NotValid => e
|
32
|
+
return bad_request(e.message)
|
33
|
+
end
|
34
|
+
|
35
|
+
status, headers, body = @app.call(env)
|
36
|
+
headers[HEADER_NAME] = @service_name
|
37
|
+
[status, headers, body]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def exclude?(path)
|
43
|
+
@exclude_paths.each do |regex|
|
44
|
+
return true if regex.match?(path)
|
45
|
+
end
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
# build out regular expression to match exclude path
|
50
|
+
def path_to_regex(exclude_paths)
|
51
|
+
reg_ex_path = []
|
52
|
+
exclude_paths.each do |path|
|
53
|
+
path_parts = path.split('/')
|
54
|
+
regex_paths = path_parts.inject do |out, p|
|
55
|
+
val = p
|
56
|
+
val = '(\\S*)' if p.include?(':')
|
57
|
+
out + '[/]' + val
|
58
|
+
end
|
59
|
+
reg_ex_path.push(Regexp.new(regex_paths))
|
60
|
+
end
|
61
|
+
reg_ex_path
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
require_relative 'railtie' if defined?(Rails)
|
5
|
+
|
6
|
+
module Spokes
|
7
|
+
module Versioning
|
8
|
+
# Minor versioning mix-in for controllers.
|
9
|
+
#
|
10
|
+
# Usage:
|
11
|
+
#
|
12
|
+
# # app/controllers/my_controller.rb
|
13
|
+
# class MyController
|
14
|
+
# include MinorVersioning
|
15
|
+
#
|
16
|
+
# def index
|
17
|
+
# logger.info(minor_version)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
module MinorVersioning
|
22
|
+
extend ActiveSupport::Concern
|
23
|
+
|
24
|
+
API_VERSION = 'API-Version'.freeze
|
25
|
+
|
26
|
+
included do
|
27
|
+
include MinorVersioning
|
28
|
+
after_filter :set_minor_version_response_header
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_minor_version_response_header
|
32
|
+
response.headers[API_VERSION] = minor_version
|
33
|
+
end
|
34
|
+
|
35
|
+
def minor_version
|
36
|
+
@minor_version ||= begin
|
37
|
+
chosen_version = request.headers[API_VERSION]
|
38
|
+
return chosen_version if valid_minor_version?(chosen_version)
|
39
|
+
default_minor_version
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def default_minor_version
|
46
|
+
@default_minor_version ||= begin
|
47
|
+
default = find_default_version
|
48
|
+
raise('No version marked as default in the configuration.') if default.nil?
|
49
|
+
default
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_default_version
|
54
|
+
all_minor_versions.each do |version, info|
|
55
|
+
return version if info[:default]
|
56
|
+
end
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def valid_minor_version?(version)
|
61
|
+
version.present? && all_minor_versions.keys.include?(version)
|
62
|
+
end
|
63
|
+
|
64
|
+
def all_minor_versions
|
65
|
+
has_versions = Rails.application.config.respond_to?(:minor_versions)
|
66
|
+
raise('config/minor_versions.yml doesn\'t exist.') unless has_versions
|
67
|
+
Rails.application.config.minor_versions
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
|
5
|
+
module Spokes
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
initializer('spokes_versioning.load_minor_versions_file') do |app|
|
8
|
+
config_file = Rails.root.join('config', 'minor_versions.yml')
|
9
|
+
if File.exist?(config_file)
|
10
|
+
minor_versions = YAML.load_file(config_file)
|
11
|
+
app.config.minor_versions = {}
|
12
|
+
minor_versions.map do |key, val|
|
13
|
+
app.config.minor_versions[key] = val.symbolize_keys
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
rake_tasks do
|
19
|
+
load 'spokes/versioning/tasks/minor_versioning.rake'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
namespace :spokes do
|
4
|
+
namespace :versioning do
|
5
|
+
MINOR_VERSION_YML = File.join(File.dirname(__FILE__), '../config/minor_versions.yml').to_s.freeze
|
6
|
+
|
7
|
+
desc 'Sets up minor versioning yml'
|
8
|
+
task setup: :environment do
|
9
|
+
raise 'Minor version currently only supports Rails Applications' unless defined?(Rails)
|
10
|
+
next if File.exist?("#{Rails.root}/config/minor_versions.yml")
|
11
|
+
FileUtils.cp(MINOR_VERSION_YML, "#{Rails.root}/config", verbose: true)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
Binary file
|
data/script/cibuild
ADDED
data/script/postbuild
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# script/postbuild: Cleanup environment after CI. This is primarily
|
4
|
+
# designed to run on the continuous integration server.
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
PROJECT_NAME='spokes'
|
9
|
+
|
10
|
+
[[ "${PROJECT_NAME:-}" ]] || (echo "PROJECT_NAME is required." && exit 1)
|
11
|
+
|
12
|
+
# cd to project root
|
13
|
+
cd "$(dirname "$0")/.."
|
14
|
+
|
15
|
+
docker stop `docker ps -a -q -f status=exited` &> /dev/null || true &> /dev/null
|
16
|
+
docker rm -v `docker ps -a -q -f status=exited` &> /dev/null || true &> /dev/null
|
17
|
+
docker rmi `docker images --filter 'dangling=true' -q --no-trunc` &> /dev/null || true &> /dev/null
|
data/script/test
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# script/test: Run test suite for application. Optionally pass in a path to an
|
4
|
+
# individual test file to run a single test.
|
5
|
+
|
6
|
+
set -e
|
7
|
+
set -u
|
8
|
+
|
9
|
+
export RAILS_ENV="test" RACK_ENV="test"
|
10
|
+
PROJECT_NAME='spokes'
|
11
|
+
|
12
|
+
[[ "${PROJECT_NAME:-}" ]] || (echo "PROJECT_NAME is required." && exit 1)
|
13
|
+
|
14
|
+
# cd to project root
|
15
|
+
cd "$(dirname "$0")/.."
|
16
|
+
|
17
|
+
# Build deploy Docker image
|
18
|
+
docker build --tag=$PROJECT_NAME .
|
19
|
+
|
20
|
+
printf "\n===> Running tests ...\n"
|
21
|
+
date "+%H:%M:%S"
|
22
|
+
|
23
|
+
docker run --rm $PROJECT_NAME bundle exec rspec
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Spokes::Config::Env do
|
4
|
+
after do
|
5
|
+
%w[HOMER_SIMPSON TEST_STRING_VAR TEST_INT_VAR TEST_FLOAT_VAR TEST_ARRAY_VAR TEST_SYMBOL_VAR TEST_BOOL_VAR].each do |w|
|
6
|
+
ENV.delete(w)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#manditory' do
|
11
|
+
let(:env_loader) do
|
12
|
+
lambda do
|
13
|
+
Spokes::Config::Env.load do
|
14
|
+
mandatory :homer_simpson, string
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'raises error if variable is not set' do
|
20
|
+
expect(env_loader).to raise_error(KeyError)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'exposes variable once loaded' do
|
24
|
+
ENV['HOMER_SIMPSON'] = 'doh'
|
25
|
+
expect(env_loader.call.homer_simpson).to eq('doh')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#optional' do
|
30
|
+
let(:env_loader) do
|
31
|
+
lambda do
|
32
|
+
Spokes::Config::Env.load do
|
33
|
+
optional :homer_simpson, string
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'exposes variable once loaded' do
|
39
|
+
ENV['HOMER_SIMPSON'] = 'doh'
|
40
|
+
expect(env_loader.call.homer_simpson).to eq('doh')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'value is nil when variable is not set' do
|
44
|
+
expect(env_loader.call.homer_simpson).to eq(nil)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#default' do
|
49
|
+
let(:env_loader) do
|
50
|
+
lambda do
|
51
|
+
Spokes::Config::Env.load do
|
52
|
+
default :homer_simpson, 'derp', string
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'exposes variable once loaded' do
|
58
|
+
ENV['HOMER_SIMPSON'] = 'doh'
|
59
|
+
expect(env_loader.call.homer_simpson).to eq('doh')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'uses default value when varible is not set' do
|
63
|
+
expect(env_loader.call.homer_simpson).to eq('derp')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'type casting' do
|
68
|
+
let(:env_loader) do
|
69
|
+
lambda do
|
70
|
+
Spokes::Config::Env.load do
|
71
|
+
optional :test_bool_var, bool
|
72
|
+
optional :test_float_var, float
|
73
|
+
optional :test_string_var, string
|
74
|
+
optional :test_symbol_var, symbol
|
75
|
+
optional :test_array_var, array
|
76
|
+
optional :test_int_var, int
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'does nothing to strings' do
|
82
|
+
ENV['TEST_STRING_VAR'] = 'stringy'
|
83
|
+
expect(env_loader.call.test_string_var).to eq('stringy')
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'casts to integer' do
|
87
|
+
ENV['TEST_INT_VAR'] = '123'
|
88
|
+
expect(env_loader.call.test_int_var).to eq(123)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'casts to float' do
|
92
|
+
ENV['TEST_FLOAT_VAR'] = '1.23'
|
93
|
+
expect(env_loader.call.test_float_var).to eq(1.23)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'casts to array' do
|
97
|
+
ENV['TEST_ARRAY_VAR'] = 'hello,world'
|
98
|
+
expect(env_loader.call.test_array_var).to eq(%w[hello world])
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'casts to symbol' do
|
102
|
+
ENV['TEST_SYMBOL_VAR'] = 'howdy'
|
103
|
+
expect(env_loader.call.test_symbol_var).to eq(:howdy)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'casts to boolean' do
|
107
|
+
ENV['TEST_BOOL_VAR'] = 'true'
|
108
|
+
expect(env_loader.call.test_bool_var).to eq(true)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Spokes::Middleware::CORS, modules: true, middleware: true do
|
4
|
+
let(:app) { proc { [200, {}, ['hi']] } }
|
5
|
+
let(:stack) { Spokes::Middleware::CORS.new(app) }
|
6
|
+
let(:request) { Rack::MockRequest.new(stack) }
|
7
|
+
|
8
|
+
it 'does not do anything when the Origin header is not present' do
|
9
|
+
response = request.get('/')
|
10
|
+
expect(response.status).to eq(200)
|
11
|
+
expect(response.body).to eq('hi')
|
12
|
+
expect(response.headers['Access-Control-Allow-Origin']).to eq(nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'intercepts OPTION requests to render a stub (preflight request)' do
|
16
|
+
response = request.options('/', 'Origin' => 'http://localhost', 'HTTP_ORIGIN' => 'http://localhost')
|
17
|
+
expect(response.status).to eq(200)
|
18
|
+
expect(response.body).to eq('')
|
19
|
+
expect(response.headers['Access-Control-Allow-Methods']).to eq('GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
20
|
+
expect(response.headers['Access-Control-Allow-Origin']).to eq('http://localhost')
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'delegates other calls, adding the CORS headers to the response' do
|
24
|
+
response = request.get('/', 'Origin' => 'http://localhost', 'HTTP_ORIGIN' => 'http://localhost')
|
25
|
+
expect(response.status).to eq(200)
|
26
|
+
expect(response.body).to eq('hi')
|
27
|
+
expect(response.headers['Access-Control-Allow-Origin']).to eq('http://localhost')
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Spokes::Middleware::Health, modules: true, middleware: true do
|
4
|
+
def env(url = '/', *args)
|
5
|
+
Rack::MockRequest.env_for(url, *args)
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:base_app) do
|
9
|
+
lambda do |_env|
|
10
|
+
[200, { 'Content-Type' => 'text/plain' }, ['Oi!']]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:app) { Rack::Lint.new Spokes::Middleware::Health.new(base_app, health_options) }
|
15
|
+
let(:health_options) { {} }
|
16
|
+
let(:status) { subject[0] }
|
17
|
+
let(:body) do
|
18
|
+
str = ''
|
19
|
+
subject[2].each do |s|
|
20
|
+
str += s
|
21
|
+
end
|
22
|
+
str
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'with default options' do
|
26
|
+
let(:health_options) { {} }
|
27
|
+
|
28
|
+
describe '/' do
|
29
|
+
subject { app.call env('/') }
|
30
|
+
it { expect(status).to eq(200) }
|
31
|
+
it { expect(body).to eq('Oi!') }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '/status' do
|
35
|
+
subject { app.call env('/status') }
|
36
|
+
it { expect(status).to eq(200) }
|
37
|
+
it { expect(body).to eq('OK') }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'as json' do
|
42
|
+
let(:health_options) { {} }
|
43
|
+
|
44
|
+
describe '/' do
|
45
|
+
subject { app.call env('/', 'Content-Type' => 'application/json') }
|
46
|
+
it { expect(status).to eq(200) }
|
47
|
+
it { expect(body).to eq('Oi!') }
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '/status' do
|
51
|
+
subject { app.call env('/status', 'CONTENT_TYPE' => 'application/json') }
|
52
|
+
it { expect(status).to eq(200) }
|
53
|
+
it { expect(JSON.parse(body)['status']).to eq('OK') }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'with :fail_if' do
|
58
|
+
subject { app.call env('/status') }
|
59
|
+
|
60
|
+
describe '== lambda { true }' do
|
61
|
+
let(:health_options) { { fail_if: -> { true } } }
|
62
|
+
|
63
|
+
it { expect(status).to eq(503) }
|
64
|
+
it { expect(body).to eq('FAIL') }
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '== lambda { false }' do
|
68
|
+
let(:health_options) { { fail_if: -> { false } } }
|
69
|
+
it { expect(status).to eq(200) }
|
70
|
+
it { expect(body).to eq('OK') }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'with :status_code' do
|
75
|
+
let(:status_proc) { ->(healthy) { healthy ? 202 : 404 } }
|
76
|
+
subject { app.call env('/status') }
|
77
|
+
|
78
|
+
context 'healthy' do
|
79
|
+
let(:health_options) { { fail_if: -> { false }, status_code: status_proc } }
|
80
|
+
it { expect(status).to eq(202) }
|
81
|
+
it { expect(body).to eq('OK') }
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'fail' do
|
85
|
+
let(:health_options) { { fail_if: -> { true }, status_code: status_proc } }
|
86
|
+
it { expect(status).to eq(404) }
|
87
|
+
it { expect(body).to eq('FAIL') }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'with :simple' do
|
92
|
+
let(:body_proc) { ->(healthy) { healthy ? 'GOOD' : 'BAD' } }
|
93
|
+
subject { app.call env('/status') }
|
94
|
+
|
95
|
+
context 'healthy' do
|
96
|
+
let(:health_options) { { fail_if: -> { false }, simple: body_proc } }
|
97
|
+
it { expect(status).to eq(200) }
|
98
|
+
it { expect(body).to eq('GOOD') }
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'fail' do
|
102
|
+
let(:health_options) { { fail_if: -> { true }, simple: body_proc } }
|
103
|
+
it { expect(status).to eq(503) }
|
104
|
+
it { expect(body).to eq('BAD') }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|