apress-api 1.22.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.drone.yml +30 -0
- data/.gitignore +15 -0
- data/.rspec +4 -0
- data/Appraisals +31 -0
- data/CHANGELOG.md +227 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +2 -0
- data/app/controllers/apress/api/deprecated_versions_controller.rb +15 -0
- data/app/controllers/apress/api/v1/callbacks_controller.rb +30 -0
- data/app/controllers/apress/api/v1/tokens_controller.rb +24 -0
- data/app/docs/schema/api/v1/types/apress/api/link.rb +29 -0
- data/app/docs/schema/api/v1/types/apress/api/links.rb +24 -0
- data/app/docs/swagger/v1/controllers/apress/api/tokens_controller.rb +64 -0
- data/app/docs/swagger/v1/default_responses/bad_request.rb +16 -0
- data/app/docs/swagger/v1/default_responses/not_found.rb +16 -0
- data/app/docs/swagger/v1/default_responses/unauthenticated.rb +16 -0
- data/app/docs/swagger/v1/default_responses/unauthorized.rb +16 -0
- data/app/docs/swagger/v1/default_responses/unprocessable.rb +16 -0
- data/app/docs/swagger/v1/default_responses/updates_locked.rb +13 -0
- data/app/docs/swagger/v1/models/apress/api/client.rb +53 -0
- data/app/docs/swagger/v1/models/apress/api/simple_error.rb +27 -0
- data/app/docs/swagger/v1/models/apress/api/unproccesable_error.rb +16 -0
- data/app/docs/swagger/v1/models/apress/api/unprocessable_error.rb +37 -0
- data/app/docs/swagger/v1/root.rb +28 -0
- data/app/interactors/apress/api/callbacks/base_callback.rb +42 -0
- data/app/interactors/apress/api/delayed_fire_callback.rb +25 -0
- data/app/jobs/apress/api/event_handler_enqueueing_job.rb +19 -0
- data/app/jobs/apress/api/fire_callback_job.rb +25 -0
- data/app/models/apress/api/client.rb +60 -0
- data/app/policies/apress/api/callback_policy.rb +15 -0
- data/app/services/apress/api/auth_service.rb +37 -0
- data/app/views/apress/api/shared/_exception.json.jbuilder +4 -0
- data/app/views/apress/api/shared/error.json.jbuilder +5 -0
- data/app/views/apress/api/shared/parameter_missing_errors.json.jbuilder +9 -0
- data/app/views/apress/api/shared/unprocessable_errors.json.jbuilder +9 -0
- data/app/views/apress/api/v1/clients/_client.json.jbuilder +2 -0
- data/app/views/apress/api/v1/tokens/create.json.jbuilder +4 -0
- data/apress-api.gemspec +46 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20150716000000_create_api_clients.rb +38 -0
- data/dip.yml +48 -0
- data/docker-compose.development.yml +18 -0
- data/docker-compose.drone.yml +7 -0
- data/docker-compose.yml +20 -0
- data/lib/apress-api.rb +1 -0
- data/lib/apress/api.rb +25 -0
- data/lib/apress/api/api_controller/authentification.rb +33 -0
- data/lib/apress/api/api_controller/base.rb +63 -0
- data/lib/apress/api/api_controller/compatibility.rb +26 -0
- data/lib/apress/api/api_controller/pagination.rb +60 -0
- data/lib/apress/api/api_controller/pagination_helper.rb +55 -0
- data/lib/apress/api/api_controller/rescue.rb +89 -0
- data/lib/apress/api/api_controller/responds.rb +25 -0
- data/lib/apress/api/callbacks/config.rb +56 -0
- data/lib/apress/api/callbacks/fire_callback_error.rb +12 -0
- data/lib/apress/api/callbacks/integration.rb +28 -0
- data/lib/apress/api/callbacks/repeat_callback_error.rb +12 -0
- data/lib/apress/api/engine.rb +33 -0
- data/lib/apress/api/extensions/jbuilder/jbuilder_template.rb +41 -0
- data/lib/apress/api/rspec.rb +48 -0
- data/lib/apress/api/rspec/utils.rb +17 -0
- data/lib/apress/api/testing/json_matcher.rb +9 -0
- data/lib/apress/api/version.rb +5 -0
- data/lib/tasks/docs.rake +12 -0
- data/spec/controllers/api_controller/authentification_spec.rb +79 -0
- data/spec/controllers/api_controller/pagination_spec.rb +199 -0
- data/spec/controllers/api_controller/rescue_spec.rb +167 -0
- data/spec/controllers/deprecated_versions_controller_spec.rb +10 -0
- data/spec/controllers/v1/callbacks_controller_spec.rb +50 -0
- data/spec/controllers/v1/tokens_controller_spec.rb +53 -0
- data/spec/factories/client_factory.rb +4 -0
- data/spec/helpers/paginating_cache_spec.rb +72 -0
- data/spec/interactors/apress/api/delayed_fire_callback_spec.rb +43 -0
- data/spec/internal/app/integrations/error_client/fire_callback.rb +14 -0
- data/spec/internal/app/integrations/service_client/fire_callback.rb +7 -0
- data/spec/internal/app/jobs/handler_job.rb +5 -0
- data/spec/internal/app/jobs/second_handler_job.rb +5 -0
- data/spec/internal/app/models/dummy_model.rb +15 -0
- data/spec/internal/config/database.yml +5 -0
- data/spec/internal/config/environments/test.rb +5 -0
- data/spec/internal/config/initializers/api.rb +10 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +5 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/jobs/apress/api/event_handler_equeueing_job_spec.rb +31 -0
- data/spec/jobs/apress/api/fire_callback_job_spec.rb +34 -0
- data/spec/lib/apress/api/callbacks/integration_spec.rb +24 -0
- data/spec/models/client_spec.rb +25 -0
- data/spec/services/auth_service_spec.rb +64 -0
- data/spec/spec_helper.rb +34 -0
- metadata +518 -0
@@ -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
|
data/lib/tasks/docs.rake
ADDED
@@ -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
|