apress-api 1.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.drone.yml +30 -0
  3. data/.gitignore +15 -0
  4. data/.rspec +4 -0
  5. data/Appraisals +31 -0
  6. data/CHANGELOG.md +227 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +31 -0
  10. data/Rakefile +2 -0
  11. data/app/controllers/apress/api/deprecated_versions_controller.rb +15 -0
  12. data/app/controllers/apress/api/v1/callbacks_controller.rb +30 -0
  13. data/app/controllers/apress/api/v1/tokens_controller.rb +24 -0
  14. data/app/docs/schema/api/v1/types/apress/api/link.rb +29 -0
  15. data/app/docs/schema/api/v1/types/apress/api/links.rb +24 -0
  16. data/app/docs/swagger/v1/controllers/apress/api/tokens_controller.rb +64 -0
  17. data/app/docs/swagger/v1/default_responses/bad_request.rb +16 -0
  18. data/app/docs/swagger/v1/default_responses/not_found.rb +16 -0
  19. data/app/docs/swagger/v1/default_responses/unauthenticated.rb +16 -0
  20. data/app/docs/swagger/v1/default_responses/unauthorized.rb +16 -0
  21. data/app/docs/swagger/v1/default_responses/unprocessable.rb +16 -0
  22. data/app/docs/swagger/v1/default_responses/updates_locked.rb +13 -0
  23. data/app/docs/swagger/v1/models/apress/api/client.rb +53 -0
  24. data/app/docs/swagger/v1/models/apress/api/simple_error.rb +27 -0
  25. data/app/docs/swagger/v1/models/apress/api/unproccesable_error.rb +16 -0
  26. data/app/docs/swagger/v1/models/apress/api/unprocessable_error.rb +37 -0
  27. data/app/docs/swagger/v1/root.rb +28 -0
  28. data/app/interactors/apress/api/callbacks/base_callback.rb +42 -0
  29. data/app/interactors/apress/api/delayed_fire_callback.rb +25 -0
  30. data/app/jobs/apress/api/event_handler_enqueueing_job.rb +19 -0
  31. data/app/jobs/apress/api/fire_callback_job.rb +25 -0
  32. data/app/models/apress/api/client.rb +60 -0
  33. data/app/policies/apress/api/callback_policy.rb +15 -0
  34. data/app/services/apress/api/auth_service.rb +37 -0
  35. data/app/views/apress/api/shared/_exception.json.jbuilder +4 -0
  36. data/app/views/apress/api/shared/error.json.jbuilder +5 -0
  37. data/app/views/apress/api/shared/parameter_missing_errors.json.jbuilder +9 -0
  38. data/app/views/apress/api/shared/unprocessable_errors.json.jbuilder +9 -0
  39. data/app/views/apress/api/v1/clients/_client.json.jbuilder +2 -0
  40. data/app/views/apress/api/v1/tokens/create.json.jbuilder +4 -0
  41. data/apress-api.gemspec +46 -0
  42. data/config/routes.rb +17 -0
  43. data/db/migrate/20150716000000_create_api_clients.rb +38 -0
  44. data/dip.yml +48 -0
  45. data/docker-compose.development.yml +18 -0
  46. data/docker-compose.drone.yml +7 -0
  47. data/docker-compose.yml +20 -0
  48. data/lib/apress-api.rb +1 -0
  49. data/lib/apress/api.rb +25 -0
  50. data/lib/apress/api/api_controller/authentification.rb +33 -0
  51. data/lib/apress/api/api_controller/base.rb +63 -0
  52. data/lib/apress/api/api_controller/compatibility.rb +26 -0
  53. data/lib/apress/api/api_controller/pagination.rb +60 -0
  54. data/lib/apress/api/api_controller/pagination_helper.rb +55 -0
  55. data/lib/apress/api/api_controller/rescue.rb +89 -0
  56. data/lib/apress/api/api_controller/responds.rb +25 -0
  57. data/lib/apress/api/callbacks/config.rb +56 -0
  58. data/lib/apress/api/callbacks/fire_callback_error.rb +12 -0
  59. data/lib/apress/api/callbacks/integration.rb +28 -0
  60. data/lib/apress/api/callbacks/repeat_callback_error.rb +12 -0
  61. data/lib/apress/api/engine.rb +33 -0
  62. data/lib/apress/api/extensions/jbuilder/jbuilder_template.rb +41 -0
  63. data/lib/apress/api/rspec.rb +48 -0
  64. data/lib/apress/api/rspec/utils.rb +17 -0
  65. data/lib/apress/api/testing/json_matcher.rb +9 -0
  66. data/lib/apress/api/version.rb +5 -0
  67. data/lib/tasks/docs.rake +12 -0
  68. data/spec/controllers/api_controller/authentification_spec.rb +79 -0
  69. data/spec/controllers/api_controller/pagination_spec.rb +199 -0
  70. data/spec/controllers/api_controller/rescue_spec.rb +167 -0
  71. data/spec/controllers/deprecated_versions_controller_spec.rb +10 -0
  72. data/spec/controllers/v1/callbacks_controller_spec.rb +50 -0
  73. data/spec/controllers/v1/tokens_controller_spec.rb +53 -0
  74. data/spec/factories/client_factory.rb +4 -0
  75. data/spec/helpers/paginating_cache_spec.rb +72 -0
  76. data/spec/interactors/apress/api/delayed_fire_callback_spec.rb +43 -0
  77. data/spec/internal/app/integrations/error_client/fire_callback.rb +14 -0
  78. data/spec/internal/app/integrations/service_client/fire_callback.rb +7 -0
  79. data/spec/internal/app/jobs/handler_job.rb +5 -0
  80. data/spec/internal/app/jobs/second_handler_job.rb +5 -0
  81. data/spec/internal/app/models/dummy_model.rb +15 -0
  82. data/spec/internal/config/database.yml +5 -0
  83. data/spec/internal/config/environments/test.rb +5 -0
  84. data/spec/internal/config/initializers/api.rb +10 -0
  85. data/spec/internal/config/routes.rb +3 -0
  86. data/spec/internal/db/schema.rb +5 -0
  87. data/spec/internal/log/.gitignore +1 -0
  88. data/spec/jobs/apress/api/event_handler_equeueing_job_spec.rb +31 -0
  89. data/spec/jobs/apress/api/fire_callback_job_spec.rb +34 -0
  90. data/spec/lib/apress/api/callbacks/integration_spec.rb +24 -0
  91. data/spec/models/client_spec.rb +25 -0
  92. data/spec/services/auth_service_spec.rb +64 -0
  93. data/spec/spec_helper.rb +34 -0
  94. metadata +518 -0
@@ -0,0 +1,12 @@
1
+ module Apress
2
+ module Api
3
+ module Callbacks
4
+ class RepeatCallbackError < StandardError
5
+ def initialize(message, backtrace)
6
+ super(message)
7
+ set_backtrace(backtrace)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ require 'rails/engine'
2
+
3
+ module Apress
4
+ module Api
5
+ class Engine < Rails::Engine
6
+ config.autoload_paths << config.root.join("lib")
7
+ config.paths.add 'app/docs', eager_load: false
8
+
9
+ Apress::Documentation.add_load_path(config.root.join('app/docs'))
10
+
11
+ initializer "apress-api", before: :load_init_rb do |app|
12
+ app.config.paths["db/migrate"].concat(config.paths["db/migrate"].expanded)
13
+
14
+ app.config.api = {
15
+ secret_token_ttl: 1.hour,
16
+ refresh_token_ttl: 1.week,
17
+ v1_doc_path: 'docs/swagger/v1.json'
18
+ }
19
+
20
+ ::MultiJson.use :oj
21
+
22
+ require 'jbuilder/jbuilder_template'
23
+ JbuilderTemplate.send :include, Apress::Api::Extensions::Jbuilder::JbuilderTemplate
24
+ end
25
+
26
+ initializer "apress-api-factories", after: "factory_girl.set_factory_paths" do
27
+ if defined?(FactoryGirl)
28
+ FactoryGirl.definition_file_paths.unshift root.join("spec", "factories")
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ module Apress
2
+ module Api
3
+ module Extensions
4
+ module Jbuilder
5
+ module JbuilderTemplate
6
+ def paginating_cache!(collection, key = nil, options = nil)
7
+ if @context.controller.perform_caching
8
+ result = Rails.cache.fetch(_cache_key(key || collection, options), options) do
9
+ {
10
+ headers: _pagination_headers(collection),
11
+ content: _scope { yield self }
12
+ }
13
+ end
14
+
15
+ _set_pagination_headers(result[:headers])
16
+
17
+ merge! result[:content]
18
+ else
19
+ _set_pagination_headers(_pagination_headers(collection))
20
+
21
+ yield
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def _set_pagination_headers(headers)
28
+ @context.controller.response.headers.merge!(headers)
29
+ end
30
+
31
+ def _pagination_headers(collection)
32
+ ::Apress::Api::ApiController::PaginationHelper.headers(
33
+ collection,
34
+ @context.controller.request.url
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ # coding: utf-8
2
+ require 'rspec/core'
3
+ require 'rspec/expectations'
4
+ require 'swagger'
5
+
6
+ require_relative 'rspec/utils'
7
+
8
+ RSpec::Matchers.define :correspond_to_schema do |schema, options|
9
+ match do |response|
10
+ if schema.present?
11
+ options ||= {}
12
+ JSON::Validator.validate!(schema, response.body.to_s, options)
13
+ else
14
+ true
15
+ end
16
+ end
17
+ end
18
+
19
+ # NOTE: Для того, чтобы находить нужную спецификацию ресурса по конкретному запросу (request.path),
20
+ # нужно уметь находить соответсвие Path -> URITemplatePath
21
+ RSpec.shared_examples 'an api response' do |status|
22
+ let(:method) { request.method.downcase.to_sym }
23
+ let(:schema) do
24
+ begin
25
+ schema = @api.paths[resource][method].responses[response.code].schema
26
+ schema.definitions = @api.definitions if schema.present?
27
+ schema
28
+ rescue
29
+ fail "Swagger schema does not exist: #{dump_response}"
30
+ end
31
+ end
32
+
33
+ it { expect(response).to have_http_status(status).and correspond_to_schema(schema) }
34
+ end
35
+
36
+ RSpec.configure do |c|
37
+ c.include Apress::Api::Rspec::Utils, type: :request
38
+
39
+ c.before(:context, :api) do
40
+ classes = Apress::Api::Swagger::Schema.swagger_classes
41
+ @swagger_schema = ::Swagger::Blocks.build_root_json(classes)
42
+ @api = Swagger.build(@swagger_schema.to_json, format: :json)
43
+ end
44
+
45
+ c.after(:example, :verbose, type: :request) do
46
+ puts dump_response
47
+ end
48
+ end
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+
3
+ module Apress
4
+ module Api
5
+ module Rspec
6
+ module Utils
7
+ def dump_response
8
+ <<-EOS.strip_heredoc
9
+ Request: #{request.method} #{request.original_url}
10
+ Status: #{response.code}
11
+ #{JSON.pretty_generate(JSON.parse(response.body.to_s))}
12
+ EOS
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ require 'json-schema'
2
+ require 'rspec/expectations'
3
+
4
+ RSpec::Matchers.define :match_json_schema do |schema, options|
5
+ match do |json|
6
+ options = {strict: true}.merge!(options || {})
7
+ JSON::Validator.validate!(schema, json, options)
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Apress
2
+ module Api
3
+ VERSION = '1.22.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ namespace :docs do
2
+ desc 'Generate api documentation'
3
+ task generate: :environment do
4
+ filename = 'swagger_v1.json'
5
+ path = Rails.application.config.api[:v1_doc_path]
6
+ FileUtils.mkdir_p(File.dirname(path))
7
+
8
+ Apress::Api::Swagger::Generator.new(path).generate_file
9
+
10
+ puts 'Done.'
11
+ end
12
+ end
@@ -0,0 +1,79 @@
1
+ require "spec_helper"
2
+
3
+ describe Apress::Api::ApiController::Base, type: :controller do
4
+ let(:client) { create "api/client" }
5
+
6
+ context "when api client is required, by default" do
7
+ controller do
8
+ def index
9
+ render json: {status: "ok"}
10
+ end
11
+ end
12
+
13
+ describe "#authenticate" do
14
+ before do
15
+ class_double(Apress::Api::AuthService, new: auth_service).as_stubbed_const
16
+ end
17
+
18
+ context "when auth successfull" do
19
+ let(:auth_service) { double("auth_service", call: true, client: client) }
20
+
21
+ it "assigns current_api_client" do
22
+ get :index
23
+ expect(response.status).to eq 200
24
+ expect(controller.current_api_client).to eq client
25
+ end
26
+ end
27
+
28
+ context "when auth failed" do
29
+ let(:auth_service) { double("auth_service", call: false, client: nil) }
30
+
31
+ it "returns 403" do
32
+ get :index
33
+ expect(response.status).to eq 403
34
+ expect(controller.current_api_client).to be nil
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ context "when api client is optional" do
41
+ controller do
42
+ if (Rails::VERSION::MAJOR == 4 && Rails::VERSION::MINOR == 2) || Rails::VERSION::MAJOR > 4
43
+ skip_before_action :authenticate
44
+ else
45
+ skip_before_filter :authenticate
46
+ end
47
+
48
+ def index
49
+ render json: {status: "ok"}
50
+ end
51
+ end
52
+
53
+ describe "#authenticate" do
54
+ before do
55
+ class_double(Apress::Api::AuthService, new: auth_service).as_stubbed_const
56
+ end
57
+
58
+ context "when auth successfull" do
59
+ let(:auth_service) { double("auth_service", call: true, client: client) }
60
+
61
+ it "assigns current_api_client" do
62
+ get :index
63
+ expect(response.status).to eq 200
64
+ expect(controller.current_api_client).to eq client
65
+ end
66
+ end
67
+
68
+ context "when auth failed" do
69
+ let(:auth_service) { double("auth_service", call: false, client: nil) }
70
+
71
+ it "returns 200 and not assign current_api_client" do
72
+ get :index
73
+ expect(response.status).to eq 200
74
+ expect(controller.current_api_client).to be nil
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,199 @@
1
+ require "spec_helper"
2
+
3
+ def form_params(p)
4
+ if Rails::VERSION::MAJOR > 4
5
+ {params: p}
6
+ else
7
+ p
8
+ end
9
+ end
10
+
11
+ describe Apress::Api::ApiController::Base, type: :controller do
12
+ describe '#prepare_pagination' do
13
+ controller do
14
+ if (Rails::VERSION::MAJOR == 4 && Rails::VERSION::MINOR == 2) || Rails::VERSION::MAJOR > 4
15
+ skip_before_action :authenticate
16
+ else
17
+ skip_before_filter :authenticate
18
+ end
19
+
20
+ def index
21
+ prepare_pagination(per_page: {max: 100, default: 40})
22
+ head 204
23
+ end
24
+ end
25
+
26
+ context 'when params present' do
27
+ context 'when params are valid' do
28
+ it 'sets variables' do
29
+ get :index, form_params(page: 2, per_page: 5)
30
+
31
+ expect(assigns(:page)).to eq 2
32
+ expect(assigns(:per_page)).to eq 5
33
+ end
34
+ end
35
+
36
+ context 'when params are invalid' do
37
+ it 'returns 400 for negative page' do
38
+ get :index, form_params(page: -1)
39
+
40
+ expect(response.status).to eq 400
41
+ end
42
+
43
+ it 'returns 400 for > max per_page value' do
44
+ get :index, form_params(per_page: 101)
45
+
46
+ expect(response.status).to eq 400
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'when no params are set' do
52
+ it 'sets defaults' do
53
+ get :index
54
+
55
+ expect(assigns(:page)).to eq 1
56
+ expect(assigns(:per_page)).to eq 40
57
+ end
58
+ end
59
+ end
60
+
61
+ describe "#pagination_headers" do
62
+ let(:current_page) { 1 }
63
+ let(:collection) do
64
+ double(
65
+ total_entries: 30,
66
+ total_pages: 3,
67
+ per_page: 10,
68
+ current_page: current_page
69
+ )
70
+ end
71
+
72
+ context 'when there are no other values in Link header' do
73
+ controller do
74
+ if (Rails::VERSION::MAJOR == 4 && Rails::VERSION::MINOR == 2) || Rails::VERSION::MAJOR > 4
75
+ skip_before_action :authenticate
76
+ else
77
+ skip_before_filter :authenticate
78
+ end
79
+
80
+ def index
81
+ pagination_headers(collection)
82
+ render json: {status: 'ok'}
83
+ end
84
+ end
85
+
86
+ before do
87
+ allow(controller).to receive(:collection).and_return(collection)
88
+ get :index
89
+ end
90
+
91
+ it 'sets correct X-Total-Count header' do
92
+ expect(response.header['X-Total-Count']).to eq '30'
93
+ end
94
+
95
+ it 'sets correct X-Total-Pages header' do
96
+ expect(response.header['X-Total-Pages']).to eq '3'
97
+ end
98
+
99
+ it 'sets correct X-Per-Page header' do
100
+ expect(response.header['X-Per-Page']).to eq '10'
101
+ end
102
+
103
+ context 'when current page is a first page' do
104
+ it 'sets correct X-Page header' do
105
+ expect(response.header['X-Page']).to eq '1'
106
+ end
107
+
108
+ it 'does not add pagination links for first and previous pages to Link header' do
109
+ expect(response.header['Link'].to_s).not_to include('rel="first"', 'rel="prev"')
110
+ end
111
+
112
+ it 'adds pagination links for next and last pages to Link header' do
113
+ expect(response.header['Link'].to_s.split(', ')).to include(
114
+ %(<#{request.url}?page=2>; rel="next"),
115
+ %(<#{request.url}?page=3>; rel="last")
116
+ )
117
+ end
118
+ end
119
+
120
+ context 'when current page is somewhere in the middle' do
121
+ let(:current_page) { 2 }
122
+
123
+ it 'sets correct X-Page header' do
124
+ expect(response.header['X-Page']).to eq '2'
125
+ end
126
+
127
+ it 'adds pagination links for first, previous, next and last pages to Link header' do
128
+ expect(response.header['Link'].to_s.split(', ')).to include(
129
+ %(<#{request.url}?page=1>; rel="first"),
130
+ %(<#{request.url}?page=1>; rel="prev"),
131
+ %(<#{request.url}?page=3>; rel="next"),
132
+ %(<#{request.url}?page=3>; rel="last")
133
+ )
134
+ end
135
+ end
136
+
137
+ context 'when current page is a last page' do
138
+ it 'sets correct X-Page header' do
139
+ expect(response.header['X-Page']).to eq '3'
140
+ end
141
+
142
+ let(:current_page) { 3 }
143
+
144
+ it 'does not add pagination links for next and last pages to Link header' do
145
+ expect(response.header['Link'].to_s).not_to include('rel="next"', 'rel="last"')
146
+ end
147
+
148
+ it 'adds pagination links for first and previous pages to Link header' do
149
+ expect(response.header['Link'].to_s.split(', ')).to include(
150
+ %(<#{request.url}?page=1>; rel="first"),
151
+ %(<#{request.url}?page=2>; rel="prev")
152
+ )
153
+ end
154
+ end
155
+
156
+ context 'when there is only one page' do
157
+ let(:collection) do
158
+ double(
159
+ total_entries: 9,
160
+ total_pages: 1,
161
+ per_page: 10,
162
+ current_page: 1
163
+ )
164
+ end
165
+
166
+ it 'does not add pagination links to Link header' do
167
+ expect(response.header['Link'].to_s).not_to include('rel="first"', 'rel="prev"', 'rel="last"', 'rel="next"')
168
+ end
169
+ end
170
+ end
171
+
172
+ context 'when Link header already has some values' do
173
+ controller do
174
+ if (Rails::VERSION::MAJOR == 4 && Rails::VERSION::MINOR == 2) || Rails::VERSION::MAJOR > 4
175
+ skip_before_action :authenticate
176
+ else
177
+ skip_before_filter :authenticate
178
+ end
179
+
180
+ def index
181
+ headers['Link'] = '<http://test.host/help>; rel="help"'
182
+
183
+ pagination_headers(collection)
184
+ render json: {status: 'ok'}
185
+ end
186
+ end
187
+
188
+ let(:current_page) { 2 }
189
+
190
+ before do
191
+ allow(controller).to receive(:collection).and_return(collection)
192
+ end
193
+
194
+ it do
195
+ expect { get :index }.to raise_error(Apress::Api::ApiController::Pagination::LinkHeaderAppendNotImplemented)
196
+ end
197
+ end
198
+ end
199
+ end