apress-api 1.22.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.
- 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,167 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Apress::Api::ApiController::Base, type: :controller do
|
|
4
|
+
render_views
|
|
5
|
+
|
|
6
|
+
let(:client) { create "api/client" }
|
|
7
|
+
let(:json) { JSON.parse(response.body) }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
allow(controller).to receive(:authenticate)
|
|
11
|
+
allow(controller).to receive(:current_api_client).and_return(client)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "#render_error" do
|
|
15
|
+
|
|
16
|
+
context "when exception given" do
|
|
17
|
+
controller do
|
|
18
|
+
def index
|
|
19
|
+
exception = ArgumentError.new(:message)
|
|
20
|
+
exception.set_backtrace(%w(path/to/file))
|
|
21
|
+
render_error(400, exception)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it do
|
|
26
|
+
get :index
|
|
27
|
+
|
|
28
|
+
expect(response.status).to eq 400
|
|
29
|
+
expect(json["status"]).to eq 400
|
|
30
|
+
expect(json["error"]["message"]).to eq "message"
|
|
31
|
+
expect(json["error"]["backtrace"][0]).to eq "path/to/file"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context "when server error" do
|
|
36
|
+
controller do
|
|
37
|
+
def index
|
|
38
|
+
exception = ArgumentError.new(:message)
|
|
39
|
+
exception.set_backtrace(%w(path/to/file))
|
|
40
|
+
server_error(exception)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it do
|
|
45
|
+
get :index
|
|
46
|
+
|
|
47
|
+
expect(response.status).to eq 500
|
|
48
|
+
expect(json["status"]).to eq 500
|
|
49
|
+
expect(json["error"]["message"]).to eq "message"
|
|
50
|
+
expect(json["error"]["backtrace"][0]).to eq "path/to/file"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context "when no exception" do
|
|
55
|
+
controller do
|
|
56
|
+
def index
|
|
57
|
+
not_found
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it do
|
|
62
|
+
get :index
|
|
63
|
+
|
|
64
|
+
expect(response.status).to eq 404
|
|
65
|
+
expect(json["status"]).to eq 404
|
|
66
|
+
expect(json["error"]).to be nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '#unproccesable' do
|
|
72
|
+
let(:record) { build 'api/client' }
|
|
73
|
+
|
|
74
|
+
before do
|
|
75
|
+
record.errors.add(:access_id, 'empty')
|
|
76
|
+
allow(controller).to receive(:record).and_return(record)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context 'when rescued from record invalid' do
|
|
80
|
+
controller do
|
|
81
|
+
def index
|
|
82
|
+
raise ActiveRecord::RecordInvalid.new(record)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'renders errors' do
|
|
87
|
+
get :index
|
|
88
|
+
|
|
89
|
+
expect(response.status).to eq 422
|
|
90
|
+
expect(json['status']).to eq 422
|
|
91
|
+
expect(json['errors']).to eq [{"access_id" => "empty"}]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context 'when rescued from standard error' do
|
|
96
|
+
controller do
|
|
97
|
+
class CustomError < StandardError
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
rescue_from CustomError, with: :unprocessable
|
|
101
|
+
|
|
102
|
+
def index
|
|
103
|
+
raise CustomError.new("custom error")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "renders error" do
|
|
108
|
+
get :index
|
|
109
|
+
|
|
110
|
+
expect(response.status).to eq 422
|
|
111
|
+
expect(json['status']).to eq 422
|
|
112
|
+
expect(json['errors']).to eq [{"message" => "custom error"}]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
context 'when called from action' do
|
|
117
|
+
controller do
|
|
118
|
+
def index
|
|
119
|
+
unprocessable(record.errors)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'renders errors' do
|
|
124
|
+
get :index
|
|
125
|
+
|
|
126
|
+
expect(response.status).to eq 422
|
|
127
|
+
expect(json['status']).to eq 422
|
|
128
|
+
expect(json['errors']).to eq [{"access_id" => "empty"}]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
context 'when called from action with multiple errors' do
|
|
133
|
+
controller do
|
|
134
|
+
def index
|
|
135
|
+
unprocessable([record.errors, 'other' => 'error'])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'renders errors' do
|
|
140
|
+
get :index
|
|
141
|
+
|
|
142
|
+
expect(response.status).to eq 422
|
|
143
|
+
expect(json['status']).to eq 422
|
|
144
|
+
expect(json['errors']).to eq [{"access_id" => "empty"}, {'other' => 'error'}]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
context '#parameter_missing' do
|
|
150
|
+
context 'when rescued' do
|
|
151
|
+
controller do
|
|
152
|
+
def index
|
|
153
|
+
params = ActionController::Parameters.new
|
|
154
|
+
params.require(:a)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'renders errors' do
|
|
159
|
+
get :index
|
|
160
|
+
|
|
161
|
+
expect(response.status).to eq 400
|
|
162
|
+
expect(json['status']).to eq 400
|
|
163
|
+
expect(json['errors']).to eq [{"a" => "missing"}]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
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::V1::CallbacksController, type: :controller do
|
|
12
|
+
let!(:client) { create "api/client" }
|
|
13
|
+
before do
|
|
14
|
+
allow(controller).to receive(:authenticate)
|
|
15
|
+
allow(controller).to receive(:current_api_client).and_return(client)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "#create" do
|
|
19
|
+
context "when client present and allowed" do
|
|
20
|
+
before do
|
|
21
|
+
client.update_attributes(access_id: 'service_access_id')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context 'when params are valid' do
|
|
25
|
+
it 'calls enqueueing job for each handler' do
|
|
26
|
+
expect(Resque).to receive(:enqueue).with(Apress::Api::EventHandlerEnqueueingJob, 'handler_job', {})
|
|
27
|
+
expect(Resque).to receive(:enqueue).with(Apress::Api::EventHandlerEnqueueingJob, 'second_handler_job', {})
|
|
28
|
+
post :create, form_params(service: 'external_service', event: 'other_event')
|
|
29
|
+
expect(response.status).to eq 201
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context 'when job is missing' do
|
|
34
|
+
it 'raises KeyError' do
|
|
35
|
+
expect do
|
|
36
|
+
post :create, form_params(service: 'service', event: 'some_event')
|
|
37
|
+
end.to raise_error(KeyError)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
context "when client isn't allowed" do
|
|
43
|
+
it 'returns 403' do
|
|
44
|
+
post :create, form_params(service: 'service', event: 'some_event')
|
|
45
|
+
|
|
46
|
+
expect(response.status).to eq 403
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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::V1::TokensController, type: :controller do
|
|
12
|
+
render_views
|
|
13
|
+
|
|
14
|
+
let!(:client) { create "api/client" }
|
|
15
|
+
|
|
16
|
+
describe "#create" do
|
|
17
|
+
context "when client doesn't exist" do
|
|
18
|
+
it do
|
|
19
|
+
post :create, form_params(client_id: "no-name")
|
|
20
|
+
expect(response.status).to eq 404
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context "when refresh token not valid" do
|
|
25
|
+
it do
|
|
26
|
+
post :create, form_params(client_id: client.access_id, refresh_token: "bad-token")
|
|
27
|
+
expect(response.status).to eq 400
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context "when refresh token expired" do
|
|
32
|
+
it do
|
|
33
|
+
Timecop.travel(1.year.from_now)
|
|
34
|
+
post :create, form_params(client_id: client.access_id, refresh_token: client.refresh_token)
|
|
35
|
+
expect(response.status).to eq 403
|
|
36
|
+
Timecop.return
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context "when refresh token is valid" do
|
|
41
|
+
it "returns regenerated tokens" do
|
|
42
|
+
post :create, form_params(client_id: client.access_id, refresh_token: client.refresh_token)
|
|
43
|
+
expect(response.status).to eq 200
|
|
44
|
+
|
|
45
|
+
client.reload
|
|
46
|
+
json = JSON.parse(response.body)
|
|
47
|
+
|
|
48
|
+
expect(json["client"]["secret_token"]).to eq client.secret_token
|
|
49
|
+
expect(json["client"]["refresh_token"]).to eq client.refresh_token
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
SIMPLE_TEST = <<-JBUILDER
|
|
3
|
+
json.paginating_cache! @collection, nil, skip_digest: true do
|
|
4
|
+
json.title 'test'
|
|
5
|
+
end
|
|
6
|
+
JBUILDER
|
|
7
|
+
|
|
8
|
+
TEST_WITH_CACHING = <<-JBUILDER
|
|
9
|
+
json.paginating_cache! @collection, ['v1', @collection], expire_in: 30.minutes, skip_digest: true do
|
|
10
|
+
json.title 'test'
|
|
11
|
+
end
|
|
12
|
+
JBUILDER
|
|
13
|
+
|
|
14
|
+
def jbuild(source_key, collection)
|
|
15
|
+
partials = {
|
|
16
|
+
'test.json.jbuilder' => SIMPLE_TEST,
|
|
17
|
+
'test_with_cache_key.json.jbuilder' => TEST_WITH_CACHING
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
resolver = ActionView::FixtureResolver.new(partials)
|
|
21
|
+
lookup_context.view_paths = [resolver]
|
|
22
|
+
assign(:collection, collection)
|
|
23
|
+
MultiJson.load(render(template: source_key))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def view_prefix
|
|
27
|
+
if Rails::VERSION::MAJOR > 4
|
|
28
|
+
'jbuilder/views'
|
|
29
|
+
else
|
|
30
|
+
'jbuilder'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe 'paginating_cache', type: :view do
|
|
35
|
+
let(:collection) { double(total_entries: 30, total_pages: 10, per_page: 5, current_page: 2, cache_key: 'test') }
|
|
36
|
+
context 'when caching disabled' do
|
|
37
|
+
it 'sets headers and yield view' do
|
|
38
|
+
result = jbuild('test.json.jbuilder', collection)
|
|
39
|
+
expect(result['title']).to eq 'test'
|
|
40
|
+
expect(view.controller.response.headers['X-Total-Count']).to eq '30'
|
|
41
|
+
expect(view.controller.response.headers['X-Total-Pages']).to eq '10'
|
|
42
|
+
expect(view.controller.response.headers['X-Per-Page']).to eq '5'
|
|
43
|
+
expect(view.controller.response.headers['X-Page']).to eq '2'
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context 'when caching enable' do
|
|
48
|
+
before do
|
|
49
|
+
view.controller.perform_caching = true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'cache result with collection key' do
|
|
53
|
+
expect(Rails.cache).to receive(:fetch).with("#{view_prefix}/test", skip_digest: true).and_call_original
|
|
54
|
+
|
|
55
|
+
jbuild('test.json.jbuilder', collection)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'cache results with custom composite key' do
|
|
59
|
+
expect(Rails.cache).to \
|
|
60
|
+
receive(:fetch).with("#{view_prefix}/v1/test", expire_in: 30.minutes, skip_digest: true).and_call_original
|
|
61
|
+
|
|
62
|
+
jbuild('test_with_cache_key.json.jbuilder', collection)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'sets headers and yield view' do
|
|
66
|
+
result = jbuild('test.json.jbuilder', collection)
|
|
67
|
+
expect(result['title']).to eq 'test'
|
|
68
|
+
expect(view.controller.response.headers['X-Total-Count']).to eq '30'
|
|
69
|
+
expect(view.controller.response.headers['X-Total-Pages']).to eq '10'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Apress::Api::DelayedFireCallback, type: :interactor do
|
|
4
|
+
let(:result) { described_class.call(event: 'some_event', params: {test: 1}) }
|
|
5
|
+
|
|
6
|
+
describe '#call' do
|
|
7
|
+
it 'calls job with correct params' do
|
|
8
|
+
expect(Resque).to receive(:enqueue).with(
|
|
9
|
+
::Apress::Api::FireCallbackJob,
|
|
10
|
+
'service',
|
|
11
|
+
'some_event',
|
|
12
|
+
test: 1
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
result
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context 'when multiple services' do
|
|
19
|
+
before do
|
|
20
|
+
allow(Apress::Api::Callbacks::Config).to \
|
|
21
|
+
receive(:services).with('some_event').and_return(%w(service_1 service_2))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'calls 2 jobs' do
|
|
25
|
+
expect(Resque).to receive(:enqueue).with(
|
|
26
|
+
::Apress::Api::FireCallbackJob,
|
|
27
|
+
'service_1',
|
|
28
|
+
'some_event',
|
|
29
|
+
test: 1
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
expect(Resque).to receive(:enqueue).with(
|
|
33
|
+
::Apress::Api::FireCallbackJob,
|
|
34
|
+
'service_2',
|
|
35
|
+
'some_event',
|
|
36
|
+
test: 1
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
result
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module ErrorClient
|
|
2
|
+
class FireCallback < Apress::Api::Callbacks::BaseCallback
|
|
3
|
+
class RepeatError < StandardError
|
|
4
|
+
end
|
|
5
|
+
add_retry_exceptions ArgumentError
|
|
6
|
+
add_repeat_exceptions RepeatError
|
|
7
|
+
|
|
8
|
+
delegate :event, to: :context
|
|
9
|
+
def call
|
|
10
|
+
raise RepeatError if event == 'repeat_error'
|
|
11
|
+
raise ArgumentError
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|