wcc-data 0.1.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/.env.example +3 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +36 -0
- data/circle.yml +6 -0
- data/lib/wcc/data.rb +26 -0
- data/lib/wcc/data/config.rb +45 -0
- data/lib/wcc/data/enumerated_type.rb +63 -0
- data/lib/wcc/data/faraday_client_app_token_auth.rb +64 -0
- data/lib/wcc/data/mapper.rb +16 -0
- data/lib/wcc/data/mapper/attributes.rb +54 -0
- data/lib/wcc/data/mapper/json_response.rb +20 -0
- data/lib/wcc/data/mapper/rest_configuration.rb +36 -0
- data/lib/wcc/data/mapper/rest_query.rb +28 -0
- data/lib/wcc/data/model.rb +11 -0
- data/lib/wcc/data/nucleus.rb +15 -0
- data/lib/wcc/data/nucleus/address.rb +17 -0
- data/lib/wcc/data/nucleus/base.rb +6 -0
- data/lib/wcc/data/nucleus/campus.rb +47 -0
- data/lib/wcc/data/nucleus/client_app_token.rb +30 -0
- data/lib/wcc/data/nucleus/contact.rb +33 -0
- data/lib/wcc/data/nucleus/gender.rb +26 -0
- data/lib/wcc/data/nucleus/group.rb +18 -0
- data/lib/wcc/data/nucleus/marital_status.rb +41 -0
- data/lib/wcc/data/nucleus/user.rb +49 -0
- data/lib/wcc/data/rack_client_app_token_auth.rb +77 -0
- data/lib/wcc/data/response.rb +29 -0
- data/lib/wcc/data/rest_endpoint.rb +33 -0
- data/lib/wcc/data/service.rb +55 -0
- data/lib/wcc/data/version.rb +5 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/inheritable_class_attribute_examples.rb +16 -0
- data/spec/wcc/data/config_spec.rb +113 -0
- data/spec/wcc/data/enumerated_type_spec.rb +176 -0
- data/spec/wcc/data/faraday_client_app_token_auth_spec.rb +224 -0
- data/spec/wcc/data/mapper/attributes_spec.rb +94 -0
- data/spec/wcc/data/mapper/json_response_spec.rb +50 -0
- data/spec/wcc/data/mapper/rest_configuration_spec.rb +43 -0
- data/spec/wcc/data/mapper/rest_query_spec.rb +50 -0
- data/spec/wcc/data/model_spec.rb +33 -0
- data/spec/wcc/data/nucleus/campus_spec.rb +29 -0
- data/spec/wcc/data/rack_client_app_token_auth_spec.rb +219 -0
- data/spec/wcc/data/response_spec.rb +57 -0
- data/spec/wcc/data/rest_endpoint_spec.rb +71 -0
- data/spec/wcc/data/service_spec.rb +128 -0
- data/spec/wcc/data_spec.rb +25 -0
- data/wcc-data.gemspec +28 -0
- metadata +194 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module WCC::Data
|
2
|
+
|
3
|
+
class RESTEndpoint
|
4
|
+
attr_reader :service
|
5
|
+
|
6
|
+
def initialize(args={})
|
7
|
+
@service = args[:service]
|
8
|
+
end
|
9
|
+
|
10
|
+
def index(options={})
|
11
|
+
service.get(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def show(id, options={})
|
15
|
+
service.get(options.merge(uri: id.to_s))
|
16
|
+
end
|
17
|
+
|
18
|
+
def create(attrs, options={})
|
19
|
+
service.post(options.merge(body: attrs))
|
20
|
+
end
|
21
|
+
|
22
|
+
def update(id, attrs, options={})
|
23
|
+
service.patch(options.merge(uri: id.to_s, body: attrs))
|
24
|
+
end
|
25
|
+
|
26
|
+
def destroy(id, options={})
|
27
|
+
service.delete(options.merge(uri: id.to_s))
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module WCC::Data
|
2
|
+
class Service
|
3
|
+
attr_reader :uri, :connection
|
4
|
+
|
5
|
+
VERBS = Set.new(%i[get post put patch delete])
|
6
|
+
|
7
|
+
def initialize(args={})
|
8
|
+
@uri = URI(args.fetch(:uri) { "" })
|
9
|
+
@connection = args.fetch(:connection) { nil }
|
10
|
+
end
|
11
|
+
|
12
|
+
def merge(arg=nil)
|
13
|
+
right = WCC::Data.Service(arg)
|
14
|
+
self.class.new(
|
15
|
+
uri: merge_uris(uri, right.uri),
|
16
|
+
connection: right.connection || connection
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
VERBS.each do |verb|
|
21
|
+
define_method(verb) do |options|
|
22
|
+
Response.new(
|
23
|
+
connection.run_request(
|
24
|
+
verb,
|
25
|
+
merge_uris(uri, options.fetch(:uri) { "" }),
|
26
|
+
options[:body],
|
27
|
+
options[:headers]
|
28
|
+
) do |request|
|
29
|
+
request.params.update(options[:params]) if options[:params]
|
30
|
+
end
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def merge_uris(uri1, uri2)
|
38
|
+
URI(uri1) + URI(uri2)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.Service(hash_or_object)
|
44
|
+
case hash_or_object
|
45
|
+
when Service
|
46
|
+
hash_or_object
|
47
|
+
when Hash
|
48
|
+
Service.new(hash_or_object)
|
49
|
+
when NilClass
|
50
|
+
Service.new
|
51
|
+
else
|
52
|
+
raise ArgumentError
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
SPEC_DIR = File.dirname(__FILE__)
|
2
|
+
FIXTURES_DIR = File.join(SPEC_DIR, "fixtures")
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift File.join(SPEC_DIR, "..", "lib")
|
5
|
+
$LOAD_PATH.unshift SPEC_DIR
|
6
|
+
|
7
|
+
require 'dotenv'
|
8
|
+
Dotenv.load
|
9
|
+
|
10
|
+
require 'wcc/data'
|
11
|
+
require 'sidekiq'
|
12
|
+
|
13
|
+
Dir[File.join(SPEC_DIR, "support", "*.rb")].each do |support_file|
|
14
|
+
require "support/#{File.basename(support_file, ".rb")}"
|
15
|
+
end
|
16
|
+
|
17
|
+
RSpec.configure do |config|
|
18
|
+
config.run_all_when_everything_filtered = true
|
19
|
+
config.filter_run :focus
|
20
|
+
|
21
|
+
config.order = 'random'
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
shared_examples_for :inheritable_class_attributes do |name|
|
2
|
+
let(:subclass) { Class.new(subject) }
|
3
|
+
|
4
|
+
it "inherited value is the same as parent class" do
|
5
|
+
subject.send(:instance_variable_set, :"@#{name}", foo: :bar)
|
6
|
+
expect(subject.send(name)).to eq(foo: :bar)
|
7
|
+
expect(subclass.send(name)).to eq(foo: :bar)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "inherited reference is unique from the parent class" do
|
11
|
+
subject.send(:instance_variable_set, :"@#{name}", foo: :bar)
|
12
|
+
subclass.send(name)[:bar] = :baz
|
13
|
+
expect(subject.send(name)).to eq(foo: :bar)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WCC::Data::Config::Application do
|
4
|
+
let(:unit) { WCC::Data::Config::Application }
|
5
|
+
subject { unit.new(:name) }
|
6
|
+
|
7
|
+
describe "#initialize" do
|
8
|
+
it "takes a single name symbol and stores in @name" do
|
9
|
+
obj = unit.new(:name)
|
10
|
+
expect(obj.name).to eq(:name)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "takes a connection option and stores it in @connection" do
|
14
|
+
obj = unit.new(:name, connection: "foo")
|
15
|
+
expect(obj.connection).to eq("foo")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "sets to a blank Faraday::Connection" do
|
19
|
+
obj = unit.new(:name)
|
20
|
+
expect(obj.connection).to be_a(Faraday::Connection)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#uri=" do
|
25
|
+
it "converts a string to a URI and stores in @uri" do
|
26
|
+
subject.uri = "http://test"
|
27
|
+
expect(subject.uri).to be_a(URI)
|
28
|
+
expect(subject.uri.scheme).to eq("http")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "uses the URI object if set with a URI" do
|
32
|
+
subject.uri = test_value = URI("http://test")
|
33
|
+
expect(subject.uri).to eq(test_value)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#service" do
|
38
|
+
before(:each) do
|
39
|
+
subject.uri = "http://test.com/"
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns a service object based on the URI" do
|
43
|
+
obj = subject.service
|
44
|
+
expect(obj).to be_a(WCC::Data::Service)
|
45
|
+
expect(obj.uri).to eq(subject.uri)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "passes the application connection through to the service" do
|
49
|
+
obj = subject.service
|
50
|
+
expect(obj.connection).to eq(subject.connection)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "@connection" do
|
55
|
+
it "gets and sets" do
|
56
|
+
subject.connection = "foo"
|
57
|
+
expect(subject.connection).to eq("foo")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe WCC::Data::Config do
|
63
|
+
let(:unit) { WCC::Data::Config }
|
64
|
+
subject { unit.new }
|
65
|
+
|
66
|
+
|
67
|
+
describe "#applications" do
|
68
|
+
it "returns a hash" do
|
69
|
+
expect(subject.applications).to be_a(Hash)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "defaults empty keys to an application" do
|
73
|
+
expect(subject.applications[:foo]).to be_a(WCC::Data::Config::Application)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "contains a default entry for :nucleus" do
|
77
|
+
expect(subject.applications[:nucleus].uri).to eq(
|
78
|
+
URI(ENV['NUCLEUS_URL'])
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
context "with APP_CLIENT_ID and APP_CLIENT_SECRET ENV vars set" do
|
83
|
+
after do
|
84
|
+
ENV['APP_CLIENT_ID'] = ENV['APP_CLIENT_SECRET'] = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
it "sets the connection's basic auth for nucleus" do
|
88
|
+
ENV['APP_CLIENT_ID'] = "test"
|
89
|
+
ENV['APP_CLIENT_SECRET'] = "value"
|
90
|
+
|
91
|
+
header = subject.applications[:nucleus].connection.headers["Authorization"]
|
92
|
+
|
93
|
+
expect(header)
|
94
|
+
.to eq(Faraday::Request::BasicAuthentication.header("test", "value"))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "with APP_CLIENT_ID and APP_CLIENT_SECRET not both set" do
|
99
|
+
it "does not set the basic auth for nucleus" do
|
100
|
+
header = subject.applications[:nucleus].connection.headers["Authorization"]
|
101
|
+
|
102
|
+
expect(header).to be_nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "#apps alias" do
|
108
|
+
it "aliases to #applications" do
|
109
|
+
expect(subject.apps).to eq(subject.applications)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe WCC::Data::EnumeratedType do
|
4
|
+
|
5
|
+
context "with configured attributes" do
|
6
|
+
let(:type) {
|
7
|
+
Class.new(described_class) do
|
8
|
+
attributes :test1, :test2
|
9
|
+
end
|
10
|
+
}
|
11
|
+
let(:object) { type.new(data) }
|
12
|
+
let(:data) {
|
13
|
+
{
|
14
|
+
test1: "test data 1",
|
15
|
+
test2: "test data 2",
|
16
|
+
ignored: "",
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
it "adds a method for all arguments passed" do
|
21
|
+
expect(object).to respond_to(:test1)
|
22
|
+
expect(object).to respond_to(:test2)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "sets values from initialize hash" do
|
26
|
+
expect(object.test1).to eq("test data 1")
|
27
|
+
expect(object.test2).to eq("test data 2")
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "::defined_attributes" do
|
31
|
+
it "should return attributes" do
|
32
|
+
expect(type.defined_attributes).to eq([:test1, :test2])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "with child classes" do
|
38
|
+
let(:parent) {
|
39
|
+
Class.new(described_class) do
|
40
|
+
attributes :attr1
|
41
|
+
attributes :attr2
|
42
|
+
end
|
43
|
+
}
|
44
|
+
let(:child) {
|
45
|
+
Class.new(parent) do
|
46
|
+
attributes :attr3, :attr4
|
47
|
+
end
|
48
|
+
}
|
49
|
+
let(:parent_object) { parent.new(data) }
|
50
|
+
let(:child_object) { child.new(data) }
|
51
|
+
let(:data) {
|
52
|
+
{
|
53
|
+
attr1: "data1",
|
54
|
+
attr2: "data2",
|
55
|
+
attr3: "data3",
|
56
|
+
attr4: "data4",
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
describe "parent class" do
|
61
|
+
it "does not include child attributes" do
|
62
|
+
expect(parent_object).to_not respond_to(:attr3)
|
63
|
+
expect(parent_object).to_not respond_to(:attr4)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "includes both attributes" do
|
67
|
+
expect(parent_object).to respond_to(:attr1)
|
68
|
+
expect(parent_object).to respond_to(:attr2)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "sets both attributes on initialize" do
|
72
|
+
expect(parent_object.attr1).to eq("data1")
|
73
|
+
expect(parent_object.attr2).to eq("data2")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "child class" do
|
78
|
+
it "includes all defined_attributes" do
|
79
|
+
expect(child.defined_attributes).to eq([:attr1, :attr2, :attr3, :attr4])
|
80
|
+
end
|
81
|
+
|
82
|
+
it "includes parent attributes and child attributes readers" do
|
83
|
+
expect(child_object.attr1).to eq("data1")
|
84
|
+
expect(child_object.attr2).to eq("data2")
|
85
|
+
expect(child_object.attr3).to eq("data3")
|
86
|
+
expect(child_object.attr4).to eq("data4")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
context "with db, matches?, and attributes configured" do
|
93
|
+
let(:type) {
|
94
|
+
Class.new(described_class) do
|
95
|
+
attributes :id, :name, :key
|
96
|
+
|
97
|
+
def matches?(value)
|
98
|
+
[id, key].include?(value)
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.db
|
102
|
+
[
|
103
|
+
{ id: 1, name: "One", key: :one },
|
104
|
+
{ id: 2, name: "Two", key: :two },
|
105
|
+
{ id: 3, name: "Three", key: :three },
|
106
|
+
{ id: 4, name: "Four", key: :three },
|
107
|
+
]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
}
|
111
|
+
|
112
|
+
describe "::[]" do
|
113
|
+
it "returns an instance of type if match found" do
|
114
|
+
expect(type[1]).to be_a(type)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "returns nil if no match found" do
|
118
|
+
expect(type[:nil]).to be_nil
|
119
|
+
end
|
120
|
+
|
121
|
+
it "returns first match when multiple matches present" do
|
122
|
+
expect(type[:three]).to eq(type.all[2])
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "::all" do
|
127
|
+
it "returns instances of EnumeratedType for each item in db" do
|
128
|
+
expect(type.all.size).to eq(4)
|
129
|
+
expect(type.all[0]).to be_a(type)
|
130
|
+
expect(type.all[1]).to be_a(type)
|
131
|
+
expect(type.all[2]).to be_a(type)
|
132
|
+
expect(type.all[3]).to be_a(type)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "sets attributes for each record properly" do
|
136
|
+
expect(type.all[0].id).to eq(1)
|
137
|
+
expect(type.all[0].name).to eq("One")
|
138
|
+
expect(type.all[0].key).to eq(:one)
|
139
|
+
expect(type.all[1].key).to eq(:two)
|
140
|
+
expect(type.all[2].key).to eq(:three)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "caches the results" do
|
144
|
+
expect(type.all[0].object_id).to eq(type.all[0].object_id)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "::reset" do
|
149
|
+
it "clears out all and forces new object creation" do
|
150
|
+
first_id = type.all[0].object_id
|
151
|
+
type.reset
|
152
|
+
expect(type.all[0].object_id).to_not eq(first_id)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe "#initialize" do
|
158
|
+
it "allows passing no argument" do
|
159
|
+
expect { described_class.new }.to_not raise_error
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "::db" do
|
164
|
+
it "raises NotImplementedError with instructions" do
|
165
|
+
expect { described_class.db }.to raise_error(NotImplementedError, /subclass/i)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "#matches?" do
|
170
|
+
it "raises NotImplementedError with instructions" do
|
171
|
+
object = described_class.new({})
|
172
|
+
expect { object.matches?(:val) }.to raise_error(NotImplementedError, /subclass/i)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe WCC::Data::FaradayClientAppTokenAuth do
|
4
|
+
let(:app) { spy(:application) }
|
5
|
+
|
6
|
+
describe WCC::Data::FaradayClientAppTokenAuth::RedisCache do
|
7
|
+
let(:connection) { instance_spy(Redis) }
|
8
|
+
let(:connection_lambda) { -> (&blk) { blk.call(connection) } }
|
9
|
+
|
10
|
+
describe "#initialize" do
|
11
|
+
it "requires a callable for Redis connection" do
|
12
|
+
expect { described_class.new }.to raise_error(ArgumentError)
|
13
|
+
obj = described_class.new(connection_lambda)
|
14
|
+
expect(obj.connection).to eq(connection_lambda)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "allows setting a :cache_key option to change the token store" do
|
18
|
+
obj = described_class.new(connection_lambda, cache_key: "store")
|
19
|
+
expect(obj.cache_key).to eq("store")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "defaults to the value of the DEFAULT_CACHE_KEY constant" do
|
23
|
+
obj = described_class.new(connection_lambda)
|
24
|
+
expect(obj.cache_key).to eq(described_class::DEFAULT_CACHE_KEY)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#[]" do
|
29
|
+
subject(:cache) { described_class.new(connection_lambda) }
|
30
|
+
|
31
|
+
it "returns HGET on the cache_key for the specified field" do
|
32
|
+
expect(connection)
|
33
|
+
.to receive(:hget).with(cache.cache_key, "example.com").and_return("123abc")
|
34
|
+
expect(cache["example.com"]).to eq("123abc")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#[]=" do
|
39
|
+
subject(:cache) { described_class.new(connection_lambda) }
|
40
|
+
|
41
|
+
it "does HSET on the cache_key for the specified field and value" do
|
42
|
+
cache["example.com"] = "abc123"
|
43
|
+
expect(connection)
|
44
|
+
.to have_received(:hset).with(cache.cache_key, "example.com", "abc123")
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with a nil value" do
|
48
|
+
it "does HDEL on the cache_key for the specified field" do
|
49
|
+
cache["example.com"] = nil
|
50
|
+
expect(connection)
|
51
|
+
.to have_received(:hdel).with(cache.cache_key, "example.com")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#initialize" do
|
58
|
+
it "takes an app as param one and sets to @app" do
|
59
|
+
obj = described_class.new(:app)
|
60
|
+
expect(obj.send(:instance_variable_get, :@app)).to eq(:app)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "takes a :cache option" do
|
64
|
+
obj = described_class.new(:app, cache: :cache)
|
65
|
+
expect(obj.cache).to eq(:cache)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "takes a :token_factory option" do
|
69
|
+
obj = described_class.new(:app, token_factory: :factory)
|
70
|
+
expect(obj.token_factory).to eq(:factory)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "defaults :cache option to an instance of RedisCache" do
|
74
|
+
obj = described_class.new(:app)
|
75
|
+
expect(obj.cache).to be_a(described_class::RedisCache)
|
76
|
+
expect(obj.cache.connection).to eq(Sidekiq.method(:redis))
|
77
|
+
end
|
78
|
+
|
79
|
+
it "defaults :token_factory option to a callable" do
|
80
|
+
obj = described_class.new(:app)
|
81
|
+
expect(obj.token_factory).to respond_to(:call)
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "default token_factory" do
|
85
|
+
it "returns #token property from a Nucleus::ClientAppToken create" do
|
86
|
+
token = instance_spy(WCC::Data::Nucleus::ClientAppToken)
|
87
|
+
|
88
|
+
expect(WCC::Data::Nucleus::ClientAppToken)
|
89
|
+
.to receive(:create).with(hostname: "example.com").and_return(token)
|
90
|
+
expect(token).to receive(:token).and_return("abc123")
|
91
|
+
expect(described_class.new(:app).token_factory.("example.com"))
|
92
|
+
.to eq("abc123")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#call" do
|
98
|
+
let(:app) { spy(:application) }
|
99
|
+
subject(:auth) { described_class.new(app) }
|
100
|
+
let(:env) {
|
101
|
+
{
|
102
|
+
url: URI("http://example.com"),
|
103
|
+
request_headers: {},
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
before do
|
108
|
+
allow(auth)
|
109
|
+
.to receive(:token_for).with("example.com").and_return("abc123")
|
110
|
+
end
|
111
|
+
|
112
|
+
it "sets env's Authorization header using value from #token_for" do
|
113
|
+
auth.call(env)
|
114
|
+
expect(env[:request_headers]["Authorization"])
|
115
|
+
.to eq("Bearer abc123")
|
116
|
+
end
|
117
|
+
|
118
|
+
it "calls @app with env" do
|
119
|
+
expect(app).to receive(:call).with(env)
|
120
|
+
auth.call(env)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "adds an on_complete handler to @app" do
|
124
|
+
allow(app).to receive(:call).and_return(app).ordered
|
125
|
+
expect(app).to receive(:on_complete).ordered
|
126
|
+
auth.call(env)
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "on_complete handler" do
|
130
|
+
subject(:auth) { described_class.new(app, cache: cache) }
|
131
|
+
let(:cache) { instance_spy(described_class::RedisCache) }
|
132
|
+
let(:handler) {
|
133
|
+
handler = nil
|
134
|
+
allow(app).to receive(:call).and_return(app)
|
135
|
+
allow(app).to receive(:on_complete) { |&block| handler = block }
|
136
|
+
|
137
|
+
auth.call(env)
|
138
|
+
|
139
|
+
handler
|
140
|
+
}
|
141
|
+
|
142
|
+
it "removes value from cache when response is unsuccessful" do
|
143
|
+
expect(cache).to receive(:[]=).with("example.com", nil).exactly(2).times
|
144
|
+
handler.(status: 200)
|
145
|
+
handler.(status: 400)
|
146
|
+
handler.(status: 500)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "retries the request after clearing the cache on a 403 response" do
|
150
|
+
expect(app).to receive(:call).with(env).ordered
|
151
|
+
expect(cache).to receive(:[]=).with("example.com", nil).ordered
|
152
|
+
expect(app).to receive(:call).with(env).ordered
|
153
|
+
handler.(status: 403)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class FakeApp
|
158
|
+
attr_accessor :response
|
159
|
+
def initialize(response)
|
160
|
+
@response = response
|
161
|
+
end
|
162
|
+
def call(request)
|
163
|
+
@request = request
|
164
|
+
self
|
165
|
+
end
|
166
|
+
def on_complete
|
167
|
+
@request[:body] = "response"
|
168
|
+
yield(response)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "multiple 403 errors" do
|
173
|
+
subject(:auth) { described_class.new(app, cache: cache, token_factory: -> (_) { "value" }) }
|
174
|
+
let(:cache) { instance_spy(described_class::RedisCache) }
|
175
|
+
let(:app) { FakeApp.new(body: "response", status: 403) }
|
176
|
+
|
177
|
+
it "doesn't retry forever" do
|
178
|
+
auth.call(env)
|
179
|
+
end
|
180
|
+
|
181
|
+
it "resets the body to original request body" do
|
182
|
+
expect(app).to receive(:call).with(hash_including(body: "request")).twice.and_call_original
|
183
|
+
auth.call(env.merge(body: "request"))
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "#token_for" do
|
189
|
+
let(:cache) { instance_spy(described_class::RedisCache) }
|
190
|
+
subject(:auth) { described_class.new(:app, cache: cache) }
|
191
|
+
|
192
|
+
context "when cache is non-nil" do
|
193
|
+
before do
|
194
|
+
allow(cache).to receive(:[]).with("example.com").and_return("abc123")
|
195
|
+
end
|
196
|
+
|
197
|
+
it "returns value from cache" do
|
198
|
+
expect(auth.token_for("example.com")).to eq("abc123")
|
199
|
+
end
|
200
|
+
|
201
|
+
it "does not call @token_factory" do
|
202
|
+
expect(auth.token_factory).to_not receive(:call)
|
203
|
+
auth.token_for("example.com")
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
context "when cache is empty" do
|
208
|
+
before do
|
209
|
+
allow(cache).to receive(:[]).and_return(nil)
|
210
|
+
allow(auth.token_factory)
|
211
|
+
.to receive(:call).with("example.com").and_return("abc123")
|
212
|
+
end
|
213
|
+
|
214
|
+
it "calls @token_factory with passed host" do
|
215
|
+
expect(auth.token_for("example.com")).to eq("abc123")
|
216
|
+
end
|
217
|
+
|
218
|
+
it "stores the value in the cache" do
|
219
|
+
expect(cache).to receive(:[]=).with("example.com", "abc123")
|
220
|
+
auth.token_for("example.com")
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|