stitches 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +126 -0
  7. data/README.md +220 -0
  8. data/Rakefile +13 -0
  9. data/lib/stitches.rb +3 -0
  10. data/lib/stitches/api_generator.rb +97 -0
  11. data/lib/stitches/api_key.rb +59 -0
  12. data/lib/stitches/api_version_constraint.rb +32 -0
  13. data/lib/stitches/configuration.rb +77 -0
  14. data/lib/stitches/error.rb +9 -0
  15. data/lib/stitches/errors.rb +101 -0
  16. data/lib/stitches/generator_files/app/controllers/api.rb +2 -0
  17. data/lib/stitches/generator_files/app/controllers/api/api_controller.rb +19 -0
  18. data/lib/stitches/generator_files/app/controllers/api/v1.rb +2 -0
  19. data/lib/stitches/generator_files/app/controllers/api/v1/pings_controller.rb +20 -0
  20. data/lib/stitches/generator_files/app/controllers/api/v2.rb +2 -0
  21. data/lib/stitches/generator_files/app/controllers/api/v2/pings_controller.rb +20 -0
  22. data/lib/stitches/generator_files/app/models/api_client.rb +2 -0
  23. data/lib/stitches/generator_files/config/initializers/stitches.rb +14 -0
  24. data/lib/stitches/generator_files/db/migrate/create_api_clients.rb +11 -0
  25. data/lib/stitches/generator_files/db/migrate/enable_uuid_ossp_extension.rb +5 -0
  26. data/lib/stitches/generator_files/lib/tasks/generate_api_key.rake +10 -0
  27. data/lib/stitches/generator_files/spec/acceptance/ping_v1_spec.rb +46 -0
  28. data/lib/stitches/generator_files/spec/features/api_spec.rb +96 -0
  29. data/lib/stitches/railtie.rb +9 -0
  30. data/lib/stitches/render_timestamps_in_iso8601_in_json.rb +9 -0
  31. data/lib/stitches/spec.rb +4 -0
  32. data/lib/stitches/spec/api_clients.rb +5 -0
  33. data/lib/stitches/spec/be_iso_8601_utc_encoded.rb +10 -0
  34. data/lib/stitches/spec/have_api_error.rb +50 -0
  35. data/lib/stitches/spec/test_headers.rb +51 -0
  36. data/lib/stitches/valid_mime_type.rb +32 -0
  37. data/lib/stitches/version.rb +3 -0
  38. data/lib/stitches/whitelisting_middleware.rb +29 -0
  39. data/lib/stitches_norailtie.rb +17 -0
  40. data/spec/api_key_spec.rb +200 -0
  41. data/spec/api_version_constraint_spec.rb +33 -0
  42. data/spec/configuration_spec.rb +105 -0
  43. data/spec/errors_spec.rb +99 -0
  44. data/spec/spec/have_api_error_spec.rb +78 -0
  45. data/spec/spec_helper.rb +10 -0
  46. data/spec/valid_mime_type_spec.rb +166 -0
  47. data/stitches.gemspec +24 -0
  48. metadata +168 -0
@@ -0,0 +1,33 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Stitches::ApiVersionConstraint do
4
+ let(:version) { 2 }
5
+ let(:request) { double("request", headers: headers) }
6
+
7
+ subject(:constraint) { described_class.new(version) }
8
+
9
+ context "no accept header" do
10
+ let(:headers) { {} }
11
+ it "doesn't match" do
12
+ expect(constraint.matches?(request)).to eq(false)
13
+ end
14
+ end
15
+ context "accept header missing version" do
16
+ let(:headers) { { accept: "application/json" } }
17
+ it "doesn't match" do
18
+ expect(constraint.matches?(request)).to eq(false)
19
+ end
20
+ end
21
+ context "accept header has wrong version" do
22
+ let(:headers) { { accept: "application/json; version=1" } }
23
+ it "doesn't match" do
24
+ expect(constraint.matches?(request)).to eq(false)
25
+ end
26
+ end
27
+ context "accept header has correct version" do
28
+ let(:headers) { { accept: "application/json; version=2" } }
29
+ it "matcheds" do
30
+ expect(constraint.matches?(request)).to eq(true)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Stitches::Configuration do
4
+ before do
5
+ Stitches.configuration.reset_to_defaults!
6
+ end
7
+
8
+ describe "global configuration" do
9
+ let(:whitelist_regexp) { %r{foo} }
10
+ let(:custom_http_auth_scheme) { "Blah" }
11
+ let(:env_var_to_hold_api_client_primary_key) { "FOOBAR" }
12
+
13
+ it "can be configured globally" do
14
+ Stitches.configure do |config|
15
+ config.whitelist_regexp = whitelist_regexp
16
+ config.custom_http_auth_scheme = custom_http_auth_scheme
17
+ config.env_var_to_hold_api_client_primary_key = env_var_to_hold_api_client_primary_key
18
+ end
19
+
20
+ expect(Stitches.configuration.whitelist_regexp).to eq(whitelist_regexp)
21
+ expect(Stitches.configuration.custom_http_auth_scheme).to eq(custom_http_auth_scheme)
22
+ expect(Stitches.configuration.env_var_to_hold_api_client_primary_key).to eq(env_var_to_hold_api_client_primary_key)
23
+ end
24
+
25
+ it "defaults to nil for whitelist_regexp" do
26
+ expect(Stitches.configuration.whitelist_regexp).to be_nil
27
+ end
28
+
29
+ it "sets a default for env_var_to_hold_api_client_primary_key" do
30
+ expect(Stitches.configuration.env_var_to_hold_api_client_primary_key).to eq("STITCHES_API_CLIENT_ID")
31
+ end
32
+
33
+ it "blows up if you try to use custom_http_auth_scheme without having set it" do
34
+ expect {
35
+ Stitches.configuration.custom_http_auth_scheme
36
+ }.to raise_error(/you must set a value for custom_http_auth_scheme/i)
37
+ end
38
+ end
39
+ describe "whitelist_regexp" do
40
+ let(:config) { Stitches::Configuration.new }
41
+ it "must be a regexp" do
42
+ expect {
43
+ config.whitelist_regexp = "foo"
44
+ }.to raise_error(/whitelist_regexp must be a Regexp/i)
45
+ end
46
+ it "may be nil" do
47
+ expect {
48
+ config.whitelist_regexp = nil
49
+ }.not_to raise_error
50
+ end
51
+ it "may be a regexp" do
52
+ expect {
53
+ config.whitelist_regexp = /foo/
54
+ }.not_to raise_error
55
+ end
56
+ end
57
+
58
+ describe "custom_http_auth_scheme" do
59
+ let(:config) { Stitches::Configuration.new }
60
+ it "must be a string" do
61
+ expect {
62
+ config.custom_http_auth_scheme = 42
63
+ }.to raise_error(/custom_http_auth_scheme must be a String/i)
64
+ end
65
+ it "may not be nil" do
66
+ expect {
67
+ config.custom_http_auth_scheme = nil
68
+ }.to raise_error(/custom_http_auth_scheme may not be blank/i)
69
+ end
70
+ it "may not be a blank string" do
71
+ expect {
72
+ config.custom_http_auth_scheme = " "
73
+ }.to raise_error(/custom_http_auth_scheme may not be blank/i)
74
+ end
75
+ it "may be a String" do
76
+ expect {
77
+ config.custom_http_auth_scheme = "Foobar"
78
+ }.not_to raise_error
79
+ end
80
+ end
81
+
82
+ describe "env_var_to_hold_api_client_primary_key" do
83
+ let(:config) { Stitches::Configuration.new }
84
+ it "must be a string" do
85
+ expect {
86
+ config.env_var_to_hold_api_client_primary_key = 42
87
+ }.to raise_error(/env_var_to_hold_api_client_primary_key must be a String/i)
88
+ end
89
+ it "may not be nil" do
90
+ expect {
91
+ config.env_var_to_hold_api_client_primary_key = nil
92
+ }.to raise_error(/env_var_to_hold_api_client_primary_key may not be blank/i)
93
+ end
94
+ it "may not be a blank string" do
95
+ expect {
96
+ config.env_var_to_hold_api_client_primary_key = " "
97
+ }.to raise_error(/env_var_to_hold_api_client_primary_key may not be blank/i)
98
+ end
99
+ it "may be a String" do
100
+ expect {
101
+ config.env_var_to_hold_api_client_primary_key = "Foobar"
102
+ }.not_to raise_error
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper.rb'
2
+
3
+ class MyFakeError < StandardError
4
+ end
5
+
6
+ class FakePersonHolder
7
+ include ActiveModel::Validations
8
+ attr_accessor :name, :person
9
+
10
+ validates_presence_of :name
11
+
12
+ def valid?
13
+ # doing this because we can't use validates_associated on a non-AR object, and
14
+ # our logic doesn't depend on validates_associated, per se
15
+ super.tap {
16
+ unless person.valid?
17
+ errors.add(:person,"is not valid")
18
+ end
19
+ }
20
+ end
21
+ end
22
+
23
+ class FakePerson
24
+ include ActiveModel::Validations
25
+
26
+ attr_accessor :first_name, :last_name, :age
27
+
28
+ validates_each :first_name, :last_name do |record, attr, value|
29
+ record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
30
+ end
31
+
32
+ validates_numericality_of :age
33
+ validates_presence_of :first_name
34
+ end
35
+
36
+ describe Stitches::Errors do
37
+ it "can be created from an exception" do
38
+ exception = MyFakeError.new("OH NOES!")
39
+ errors = Stitches::Errors.from_exception(exception)
40
+
41
+ expect(errors.size).to eq(1)
42
+ expect(errors.first.code).to eq("my_fake")
43
+ expect(errors.first.message).to eq("OH NOES!")
44
+ end
45
+
46
+ it "renders useful JSON" do
47
+ errors = Stitches::Errors.new([
48
+ Stitches::Error.new(code: "not_found", message: "Was not found, yo"),
49
+ Stitches::Error.new(code: "and_you_should_feel_bad", message: "And you should feel bad about even asking"),
50
+ ])
51
+
52
+ expect(errors.to_json).to eq(
53
+ [
54
+ {
55
+ "code" => "not_found",
56
+ "message" => "Was not found, yo",
57
+ },
58
+ {
59
+ "code" => "and_you_should_feel_bad",
60
+ "message" => "And you should feel bad about even asking",
61
+ }
62
+ ].to_json
63
+ )
64
+ end
65
+
66
+ context "creation from an active record object" do
67
+ let(:object) { FakePerson.new.tap { |person|
68
+ person.age = "asdfasdf"
69
+ person.last_name = "zjohnson"
70
+ }}
71
+ it "sets reasonable messages for the fields of the object" do
72
+ object.valid?
73
+ errors = Stitches::Errors.from_active_record_object(object)
74
+ errors_hash = JSON.parse(errors.to_json).sort_by {|_| _["code"] }
75
+ expect(errors_hash[0]["code"]).to eq("age_invalid")
76
+ expect(errors_hash[0]["message"]).to eq("Age is not a number")
77
+ expect(errors_hash[1]["code"]).to eq("first_name_invalid")
78
+ expect(errors_hash[1]["message"]).to eq("First name can't be blank")
79
+ expect(errors_hash[2]["code"]).to eq("last_name_invalid")
80
+ expect(errors_hash[2]["message"]).to eq("Last name starts with z.")
81
+ end
82
+
83
+ it "digs one level deep into the object for associated active-records" do
84
+ holder = FakePersonHolder.new
85
+ holder.name = nil
86
+ holder.person = object
87
+
88
+ holder.valid?
89
+
90
+ errors = Stitches::Errors.from_active_record_object(holder)
91
+ errors_hash = JSON.parse(errors.to_json).sort_by {|_| _["code"] }
92
+ expect(errors_hash[0]["code"]).to eq("name_invalid")
93
+ expect(errors_hash[0]["message"]).to eq("Name can't be blank")
94
+ expect(errors_hash[1]["code"]).to eq("person_invalid")
95
+ expect(errors_hash[1]["message"]).to eq("Age is not a number, First name can't be blank, Last name starts with z.")
96
+ end
97
+ end
98
+ end
99
+
@@ -0,0 +1,78 @@
1
+ require 'spec_helper.rb'
2
+ require 'stitches/spec'
3
+
4
+ describe "have_api_error" do
5
+ let(:errors) {
6
+ [
7
+ { code: "foo", message: "bar" },
8
+ { code: "baz", message: "quux" }
9
+ ]
10
+ }
11
+ let(:response) {
12
+ double(
13
+ response_code: response_code,
14
+ body: { errors: errors }.to_json)
15
+ }
16
+ context "missing required arguments from expectation" do
17
+ let(:response_code) { 422 }
18
+ it "blows up with a decent message" do
19
+ expect {
20
+ expect(response).to have_api_error(message: errors.first[:message])
21
+ }.to raise_error(/key not found: :code/)
22
+ expect {
23
+ expect(response).to have_api_error(code: errors.first[:code])
24
+ }.to raise_error(/key not found: :message/)
25
+ end
26
+ end
27
+ context "no expected status specified" do
28
+ context "status is 422" do
29
+ let(:response_code) { 422 }
30
+ context "an error in the expectation exists" do
31
+ it "indicates there is an error" do
32
+ expect(response).to have_api_error(errors.first)
33
+ end
34
+ it "indicates there is an error for another error in the errors list" do
35
+ expect(response).to have_api_error(errors.second)
36
+ end
37
+ it "indicates there is an error via regexp" do
38
+ expect(response).to have_api_error(code: errors.second[:code], message: /^.*$/)
39
+ end
40
+ end
41
+ context "no error from the expectation exists" do
42
+ it "indicates there is no error" do
43
+ expect(response).not_to have_api_error(code: "blah", message: "crud")
44
+ end
45
+ end
46
+ end
47
+ context "status is 200" do
48
+ let(:response_code) { 200 }
49
+ it "indicates there is no error" do
50
+ expect(response).not_to have_api_error(errors.first)
51
+ end
52
+ end
53
+ context "status is e.g. 404" do
54
+ let(:response_code) { 404 }
55
+ it "indicates there is no error" do
56
+ expect(response).not_to have_api_error(errors.first)
57
+ end
58
+ end
59
+ end
60
+ context "expected status is specified" do
61
+ context "status is the expected status" do
62
+ let(:response_code) { 404 }
63
+ it "indicates there is an error" do
64
+ expect(response).to have_api_error(status: 404,
65
+ code: errors.first[:code],
66
+ message: errors.first[:message])
67
+ end
68
+ end
69
+ context "status is not the expected status" do
70
+ let(:response_code) { 422 }
71
+ it "indicates there is no error" do
72
+ expect(response).not_to have_api_error(status: 404,
73
+ code: errors.first[:code],
74
+ message: errors.first[:bar])
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,10 @@
1
+ GEM_ROOT = File.expand_path(File.join(File.dirname(__FILE__),'..'))
2
+ Dir["#{GEM_ROOT}/spec/support/**/*.rb"].sort.each {|f| require f}
3
+
4
+ require 'rails/all'
5
+ require 'stitches'
6
+
7
+ RSpec.configure do |config|
8
+ config.order = "random"
9
+ end
10
+ I18n.enforce_available_locales = false # situps
@@ -0,0 +1,166 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Stitches::ValidMimeType do
4
+ let(:app) { double("rack app") }
5
+
6
+ before do
7
+ allow(app).to receive(:call).with(env)
8
+ end
9
+
10
+ subject(:middleware) { described_class.new(app, namespace: "/api") }
11
+
12
+ shared_examples "an unacceptable response" do
13
+ it "returns a 406" do
14
+ expect(@response.status).to eq(406)
15
+ end
16
+ it "stops the call chain preventing anything from happening" do
17
+ expect(app).not_to have_received(:call)
18
+ end
19
+ it "sends a reasonable message" do
20
+ expect(@response.body.first).to match(/didn't have the right mime type or version number. We only accept application\/json/)
21
+ end
22
+ end
23
+
24
+ describe "#call" do
25
+ context "not in namespace" do
26
+ context "not in whitelist" do
27
+ let(:env) {
28
+ {
29
+ "PATH_INFO" => "/index/home",
30
+ }
31
+ }
32
+
33
+ before do
34
+ @response = middleware.call(env)
35
+ end
36
+
37
+ it_behaves_like "an unacceptable response"
38
+ end
39
+ context "whitelisting" do
40
+ let(:env) {
41
+ {
42
+ "PATH_INFO" => "/index/home",
43
+ }
44
+ }
45
+
46
+ context "whitelist is explicit in middleware usage" do
47
+ before do
48
+ @response = middleware.call(env)
49
+ end
50
+
51
+ context "passes the whitelist" do
52
+ subject(:middleware) { described_class.new(app, except: %r{\A/resque\/.*\Z}) }
53
+ let(:env) {
54
+ {
55
+ "PATH_INFO" => "/resque/overview"
56
+ }
57
+ }
58
+ it "calls through to the rest of the chain" do
59
+ expect(app).to have_received(:call).with(env)
60
+ end
61
+ end
62
+
63
+ context "fails the whitelist" do
64
+ subject(:middleware) { described_class.new(app, except: %r{\A/resque\/.*\Z}) }
65
+ let(:env) {
66
+ {
67
+ "PATH_INFO" => "//resque/overview" # subtle
68
+ }
69
+ }
70
+ it_behaves_like "an unacceptable response"
71
+ end
72
+ context "except: is not given a regexp" do
73
+ let(:env) {
74
+ {
75
+ "PATH_INFO" => "//resque/overview"
76
+ }
77
+ }
78
+ it "blows up" do
79
+ expect {
80
+ described_class.new(app, except: "/resque")
81
+ }.to raise_error(/must be a Regexp/i)
82
+ end
83
+ end
84
+ end
85
+ context "whitelist is implicit from the configuration" do
86
+
87
+ before do
88
+ Stitches.configuration.whitelist_regexp = %r{\A/resque/.*\Z}
89
+ @response = middleware.call(env)
90
+ end
91
+
92
+ context "passes the whitelist" do
93
+ subject(:middleware) { described_class.new(app) }
94
+ let(:env) {
95
+ {
96
+ "PATH_INFO" => "/resque/overview"
97
+ }
98
+ }
99
+ it "calls through to the rest of the chain" do
100
+ expect(app).to have_received(:call).with(env)
101
+ end
102
+ end
103
+
104
+ context "fails the whitelist" do
105
+ subject(:middleware) { described_class.new(app) }
106
+ let(:env) {
107
+ {
108
+ "PATH_INFO" => "//resque/overview" # subtle
109
+ }
110
+ }
111
+ it_behaves_like "an unacceptable response"
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ context "valid header" do
118
+ let(:env) {
119
+ {
120
+ "PATH_INFO" => "/api/ping",
121
+ "HTTP_ACCEPT" => "application/json; version=99",
122
+ }
123
+ }
124
+
125
+ before do
126
+ @response = middleware.call(env)
127
+ end
128
+ it "calls through to the rest of the chain" do
129
+ expect(app).to have_received(:call).with(env)
130
+ end
131
+ end
132
+
133
+ context "unacceptable responses" do
134
+ before do
135
+ @response = middleware.call(env)
136
+ @response.finish
137
+ end
138
+ context "no header" do
139
+ let(:env) {
140
+ {
141
+ "PATH_INFO" => "/api/ping",
142
+ }
143
+ }
144
+ it_behaves_like "an unacceptable response"
145
+ end
146
+ context "bad mime type" do
147
+ let(:env) {
148
+ {
149
+ "PATH_INFO" => "/api/ping",
150
+ "HTTP_ACCEPT" => "application/json; version=bleorgh",
151
+ }
152
+ }
153
+ it_behaves_like "an unacceptable response"
154
+ end
155
+ context "bad version" do
156
+ let(:env) {
157
+ {
158
+ "PATH_INFO" => "/api/ping",
159
+ "HTTP_ACCEPT" => "application/xml; version=1",
160
+ }
161
+ }
162
+ it_behaves_like "an unacceptable response"
163
+ end
164
+ end
165
+ end
166
+ end