Skip to content

Commit 410fd9c

Browse files
committed
feat: add API.test to support testing endpoints
1 parent 87f6b6b commit 410fd9c

15 files changed

+191
-72
lines changed

doc/testing.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Testing controllers
2+
3+
You can, of course, just test your full stack by using your existing web testing framework. If you want to test endpoints directly, Rapid provides a few useful tools that you can use to faciliate this.
4+
5+
```ruby
6+
response = CoreAPI::Base.test_endpoint(described_class, :create) do |req|
7+
req.json_body[:user] = { user: 'blah' }
8+
end
9+
10+
response.status # => 201
11+
response.body # => { ... }
12+
response.headers # => { ... }
13+
```

lib/rapid/api.rb

+26
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
require 'rapid/helpers'
66
require 'rapid/manifest_errors'
77
require 'rapid/object_set'
8+
require 'rapid/errors/standard_error'
9+
require 'rapid/mock_request'
810

911
module Rapid
1012
class API
@@ -67,6 +69,30 @@ def schema(host:, namespace:)
6769
})
6870
end
6971

72+
# Execute a request for a given controller & endpoint
73+
#
74+
# @param controller [Rapid::Controller]
75+
# @param endpoint_name [Symbol]
76+
# @return [Rapid::Response]
77+
def test_endpoint(controller, endpoint)
78+
if endpoint.is_a?(Symbol) || endpoint.is_a?(String)
79+
endpoint_name = endpoint
80+
endpoint = controller.definition.endpoints[endpoint.to_sym]
81+
if endpoint.nil?
82+
raise Rapid::StandardError, "Invalid endpoint name '#{endpoint_name}' for '#{controller.name}'"
83+
end
84+
end
85+
86+
request = Rapid::MockRequest.empty
87+
request.api = self
88+
request.controller = controller
89+
request.endpoint = endpoint
90+
91+
yield request if block_given?
92+
93+
endpoint.execute(request)
94+
end
95+
7096
end
7197

7298
end

lib/rapid/definitions/polymorph_option.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def matches?(value)
2929

3030
def cast(value, request: nil, path: [])
3131
{
32-
'type' => @name.to_s,
33-
'value' => type.cast(value, request: request, path: path)
32+
type: @name.to_s,
33+
value: type.cast(value, request: request, path: path)
3434
}
3535
end
3636

lib/rapid/errors/standard_error.rb

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
module Rapid
4+
class StandardError < ::StandardError
5+
end
6+
end

lib/rapid/field_set.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def generate_hash(source, request: nil, path: [])
4646
value = field.value(source, request: request, path: field_path)
4747
next if value == :skip
4848

49-
hash[field.name.to_s] = value
49+
hash[field.name.to_sym] = value
5050
end
5151
end
5252

lib/rapid/mock_request.rb

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
require 'rapid/request'
4+
5+
module Rapid
6+
class MockRequest < Request
7+
8+
def json_body
9+
@json_body ||= {}
10+
end
11+
12+
def ip
13+
@ip ||= '127.0.0.1'
14+
end
15+
attr_writer :ip
16+
17+
end
18+
end

lib/rapid/rack.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def handle_request(env, api_path)
9393

9494
response = request.endpoint.execute(request)
9595
response.rack_triplet
96-
rescue StandardError => e
96+
rescue ::StandardError => e
9797
if e.is_a?(RackError) || e.is_a?(Rapid::ManifestError)
9898
return e.triplet
9999
end

lib/rapid/request_environment.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def call(*args, &block)
2020
return unless block_given?
2121

2222
instance_exec(@request, @response, *args, &block)
23-
rescue StandardError => e
23+
rescue ::StandardError => e
2424
raise_exception(e)
2525
end
2626

lib/rapid/request_headers.rb

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ def [](key)
1515
fetch(key)
1616
end
1717

18+
def []=(key, value)
19+
@headers[self.class.make_key(key)] = value
20+
end
21+
1822
class << self
1923

2024
def make_key(key)

spec/specs/rapid/api_spec.rb

+56-4
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,62 @@
6767
it 'should return the schema' do
6868
api = Rapid::API.create('ExampleAPI')
6969
schema = api.schema(host: 'api.example.com', namespace: 'v1')
70-
expect(schema['host']).to eq 'api.example.com'
71-
expect(schema['namespace']).to eq 'v1'
72-
expect(schema['objects']).to be_a Array
73-
expect(schema['api']).to eq 'ExampleAPI'
70+
expect(schema[:host]).to eq 'api.example.com'
71+
expect(schema[:namespace]).to eq 'v1'
72+
expect(schema[:objects]).to be_a Array
73+
expect(schema[:api]).to eq 'ExampleAPI'
74+
end
75+
end
76+
77+
context '.test_endpoint' do
78+
it 'returns an error if the endpoint name is incorrect' do
79+
api = Rapid::API.create('ExampleAPI')
80+
controller = Rapid::Controller.create('ExampleController')
81+
expect { api.test_endpoint(controller, :invalid) }.to raise_error(Rapid::StandardError, /invalid endpoint name/i)
82+
end
83+
84+
it 'executes the endpoint' do
85+
api = Rapid::API.create('ExampleAPI')
86+
controller = Rapid::Controller.create('ExampleController') do
87+
endpoint :info do
88+
field :name, :string
89+
action do
90+
response.add_field :name, 'Peter'
91+
end
92+
end
93+
end
94+
response = api.test_endpoint(controller, :info)
95+
expect(response.status).to eq 200
96+
expect(response.body[:name]).to eq 'Peter'
97+
end
98+
99+
it 'executes the endpoint through the authenticator' do
100+
api = Rapid::API.create('ExampleAPI') do
101+
authenticator do
102+
potential_error 'AccessDenied' do
103+
http_status 403
104+
code :access_denied
105+
end
106+
action do
107+
if request.headers['Authorization'] == 'Bearer test'
108+
request.identity = true
109+
else
110+
raise_error 'AccessDenied'
111+
end
112+
end
113+
end
114+
end
115+
controller = Rapid::Controller.create('ExampleController') do
116+
endpoint :info do
117+
field :name, :string
118+
action do
119+
response.add_field :name, 'Peter'
120+
end
121+
end
122+
end
123+
response = api.test_endpoint(controller, :info)
124+
expect(response.status).to eq 403
125+
expect(response.body[:error][:code]).to eq :access_denied
74126
end
75127
end
76128
end

spec/specs/rapid/definitions/field_spec.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,12 @@
165165
expect(value).to be_a Array
166166
expect(value[0]).to be_a Hash
167167

168-
expect(value[0]['name']).to eq 'Adam'
169-
expect(value[0]['age']).to eq 20
168+
expect(value[0][:name]).to eq 'Adam'
169+
expect(value[0][:age]).to eq 20
170170

171171
expect(value[1]).to be_a Hash
172-
expect(value[1]['name']).to eq 'Michael'
173-
expect(value[1]['age']).to eq 25
172+
expect(value[1][:name]).to eq 'Michael'
173+
expect(value[1][:age]).to eq 25
174174
end
175175
end
176176
end

spec/specs/rapid/endpoint_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
expect(response.body[:error]).to be_a Hash
114114
expect(response.body[:error][:code]).to eq :scope_not_granted
115115
expect(response.body[:error][:description]).to eq 'The scope required for this endpoint has not been granted to the authenticating identity'
116-
expect(response.body[:error][:detail]['scopes']).to eq ['example']
116+
expect(response.body[:error][:detail][:scopes]).to eq ['example']
117117
end
118118
end
119119

0 commit comments

Comments
 (0)