setsuzoku 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +2 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +82 -0
- data/LICENSE.txt +21 -0
- data/README.md +93 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/setsuzoku.rb +37 -0
- data/lib/setsuzoku/api_response.rb +11 -0
- data/lib/setsuzoku/api_strategy.rb +124 -0
- data/lib/setsuzoku/auth_strategy.rb +72 -0
- data/lib/setsuzoku/credential.rb +37 -0
- data/lib/setsuzoku/exception.rb +18 -0
- data/lib/setsuzoku/external_api_handler.rb +19 -0
- data/lib/setsuzoku/pluggable.rb +87 -0
- data/lib/setsuzoku/plugin.rb +128 -0
- data/lib/setsuzoku/rspec.rb +2 -0
- data/lib/setsuzoku/rspec/dynamic_spec_helper.rb +281 -0
- data/lib/setsuzoku/service.rb +70 -0
- data/lib/setsuzoku/service/web_service.rb +21 -0
- data/lib/setsuzoku/service/web_service/api_strategies/rest_api_request.rb +17 -0
- data/lib/setsuzoku/service/web_service/api_strategies/rest_strategy.rb +155 -0
- data/lib/setsuzoku/service/web_service/api_strategy.rb +169 -0
- data/lib/setsuzoku/service/web_service/auth_strategies/basic_auth_strategy.rb +50 -0
- data/lib/setsuzoku/service/web_service/auth_strategies/o_auth_strategy.rb +173 -0
- data/lib/setsuzoku/service/web_service/auth_strategy.rb +48 -0
- data/lib/setsuzoku/service/web_service/credentials/basic_auth_credential.rb +52 -0
- data/lib/setsuzoku/service/web_service/credentials/o_auth_credential.rb +83 -0
- data/lib/setsuzoku/service/web_service/service.rb +31 -0
- data/lib/setsuzoku/utilities.rb +7 -0
- data/lib/setsuzoku/version.rb +6 -0
- data/setsuzoku.gemspec +50 -0
- data/sorbet/config +2 -0
- data/sorbet/rbi/gems/activesupport.rbi +1125 -0
- data/sorbet/rbi/gems/addressable.rbi +199 -0
- data/sorbet/rbi/gems/concurrent-ruby.rbi +1586 -0
- data/sorbet/rbi/gems/crack.rbi +62 -0
- data/sorbet/rbi/gems/faraday.rbi +615 -0
- data/sorbet/rbi/gems/hashdiff.rbi +66 -0
- data/sorbet/rbi/gems/httparty.rbi +401 -0
- data/sorbet/rbi/gems/i18n.rbi +133 -0
- data/sorbet/rbi/gems/mime-types-data.rbi +17 -0
- data/sorbet/rbi/gems/mime-types.rbi +218 -0
- data/sorbet/rbi/gems/multi_xml.rbi +35 -0
- data/sorbet/rbi/gems/multipart-post.rbi +53 -0
- data/sorbet/rbi/gems/nokogiri.rbi +1011 -0
- data/sorbet/rbi/gems/public_suffix.rbi +104 -0
- data/sorbet/rbi/gems/rake.rbi +646 -0
- data/sorbet/rbi/gems/rspec-core.rbi +1893 -0
- data/sorbet/rbi/gems/rspec-expectations.rbi +1123 -0
- data/sorbet/rbi/gems/rspec-mocks.rbi +1090 -0
- data/sorbet/rbi/gems/rspec-support.rbi +280 -0
- data/sorbet/rbi/gems/rspec.rbi +15 -0
- data/sorbet/rbi/gems/safe_yaml.rbi +124 -0
- data/sorbet/rbi/gems/thread_safe.rbi +82 -0
- data/sorbet/rbi/gems/tzinfo.rbi +406 -0
- data/sorbet/rbi/gems/webmock.rbi +532 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +13722 -0
- data/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1431 -0
- data/sorbet/rbi/sorbet-typed/lib/bundler/all/bundler.rbi +8684 -0
- data/sorbet/rbi/sorbet-typed/lib/httparty/all/httparty.rbi +427 -0
- data/sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi +108 -0
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/open3.rbi +111 -0
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/resolv.rbi +543 -0
- data/sorbet/rbi/todo.rbi +11 -0
- metadata +255 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Setsuzoku
|
5
|
+
# The API Authentication Interface definition.
|
6
|
+
# Any AuthStrategy that implements this interface must implement all abstract methods defined by AuthStrategy.
|
7
|
+
#
|
8
|
+
# Defines all necessary methods for handling authentication for any authentication strategy.
|
9
|
+
module AuthStrategy
|
10
|
+
|
11
|
+
extend Forwardable
|
12
|
+
extend T::Sig
|
13
|
+
extend T::Helpers
|
14
|
+
abstract!
|
15
|
+
|
16
|
+
attr_accessor :service
|
17
|
+
attr_accessor :credential
|
18
|
+
def_delegators :@service, :plugin, :api_strategy, :external_api_handler
|
19
|
+
|
20
|
+
# Initialize the auth_strategy and provide reference to service.
|
21
|
+
#
|
22
|
+
# @param service [Service] the new instance of service with its correct strategies.
|
23
|
+
#
|
24
|
+
# @return [AuthStrategy] the new instance of auth_strategy
|
25
|
+
sig(:final) do
|
26
|
+
params(
|
27
|
+
service: T.any(
|
28
|
+
Setsuzoku::Service::WebService::Service,
|
29
|
+
T.untyped
|
30
|
+
),
|
31
|
+
args: T.untyped
|
32
|
+
).returns(T.any(
|
33
|
+
Setsuzoku::Service::WebService::AuthStrategies::BasicAuthStrategy,
|
34
|
+
Setsuzoku::Service::WebService::AuthStrategies::OAuthStrategy,
|
35
|
+
T.untyped
|
36
|
+
))
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(service:, **args)
|
40
|
+
self.service = service
|
41
|
+
credential = args[:credential]
|
42
|
+
self.credential = if self.plugin.registered_instance
|
43
|
+
credential
|
44
|
+
else
|
45
|
+
self.class.credential_class.stub_credential
|
46
|
+
end
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
# The getter method for a credential should retrieve dynamically if it's a proc.
|
51
|
+
# We cannot assign this at initialize time, as it may not yet exist on the instance.
|
52
|
+
#
|
53
|
+
# @return [Credential] the credential to use for the current requests.
|
54
|
+
def credential
|
55
|
+
self.plugin.get_registered_instance_val(@credential)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if a credential is valid.
|
59
|
+
# Additionally it should revalidate if invalid.
|
60
|
+
#
|
61
|
+
# @return [Boolean] true if the credential is valid.
|
62
|
+
sig { abstract.returns(T::Boolean) }
|
63
|
+
def auth_credential_valid?; end
|
64
|
+
|
65
|
+
# Authorize a credential for a specific auth_strategy.
|
66
|
+
# It should also set the credential attributes and save if appropriate.
|
67
|
+
#
|
68
|
+
# @return [void]
|
69
|
+
sig { abstract.params(args: T.untyped).void }
|
70
|
+
def new_credential!(**args); end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
# typed: ignore
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Setsuzoku
|
6
|
+
module Credential
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Helpers
|
9
|
+
interface!
|
10
|
+
|
11
|
+
# Create a stub implementation of the credential for testing purposes
|
12
|
+
#
|
13
|
+
# @return [Struct] a struct that implements stubbed version of a credential's required methods.
|
14
|
+
sig { abstract.returns(Struct) }
|
15
|
+
def self.stub_credential; end
|
16
|
+
|
17
|
+
# A settings object for the credential.
|
18
|
+
#
|
19
|
+
# @return [Hash] the settings for the credential.
|
20
|
+
sig { abstract.returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
|
21
|
+
def settings; end
|
22
|
+
|
23
|
+
# Setter for the settings object for the credential.
|
24
|
+
#
|
25
|
+
# @param val [Hash] a settings object to set.
|
26
|
+
#
|
27
|
+
# @return [Hash] the settings for the credential.
|
28
|
+
sig do
|
29
|
+
abstract
|
30
|
+
.params(val: T.nilable(T::Hash[T.untyped, T.untyped]))
|
31
|
+
.returns(T.nilable(T::Hash[T.untyped, T.untyped]))
|
32
|
+
end
|
33
|
+
def settings=(val); end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
module Setsuzoku
|
4
|
+
module Exception
|
5
|
+
class InvalidAuthCredentials < StandardError
|
6
|
+
def initialize(msg='The authentication credentials for this request are invalid.')
|
7
|
+
super(msg)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class UndefinedRequiredMethod < StandardError
|
12
|
+
def initialize(registering_instance:, plugin_class:, method_name:)
|
13
|
+
msg = "#{registering_instance.name} attempted to register #{plugin_class.name} but did not define the required instance method: #{method_name}"
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Setsuzoku
|
5
|
+
# The base definition for the exception handling class.
|
6
|
+
# This can be overridden by application configuration.
|
7
|
+
class ExternalApiHandler
|
8
|
+
def call_external_api_wrapper(**args)
|
9
|
+
puts('Setsuzoku API call pending')
|
10
|
+
response = yield
|
11
|
+
puts("Setsuzoku API call complete. Success status: #{response[:success]}")
|
12
|
+
end
|
13
|
+
|
14
|
+
def call_external_api_exception(**args)
|
15
|
+
puts(args[:exception].backtrace.join("\n")) if args[:exception]
|
16
|
+
puts("call_external_api failed with: #{args.inspect}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Setsuzoku
|
5
|
+
# The module to include into a class to give it plugin functionality.
|
6
|
+
module Pluggable
|
7
|
+
extend Forwardable
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
abstract!
|
11
|
+
|
12
|
+
attr_accessor :plugin
|
13
|
+
def_delegators :@plugin, :plugin_service, :plugin_request_class
|
14
|
+
|
15
|
+
def self.included(klass)
|
16
|
+
klass.extend(ClassMethods)
|
17
|
+
klass.class_eval do
|
18
|
+
if klass.respond_to?(:after_initialize)
|
19
|
+
after_initialize :assign_plugin
|
20
|
+
else
|
21
|
+
# Initialize the pluggable instance and associate its plugin.
|
22
|
+
#
|
23
|
+
# @param _args [Array] array of args passed into new. #unused
|
24
|
+
# @param _block [Proc] the block passed into new. #unused
|
25
|
+
#
|
26
|
+
# @return [Class] the instance of the class that is pluggable.
|
27
|
+
def initialize(*_args, &_block)
|
28
|
+
super
|
29
|
+
self.assign_plugin
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
extend T::Sig
|
38
|
+
|
39
|
+
def plugin_context
|
40
|
+
@plugin_context
|
41
|
+
end
|
42
|
+
def plugin_context=(val)
|
43
|
+
@plugin_context = val
|
44
|
+
end
|
45
|
+
def plugin_class
|
46
|
+
@plugin_class
|
47
|
+
end
|
48
|
+
def plugin_class=(val)
|
49
|
+
@plugin_class = val
|
50
|
+
end
|
51
|
+
|
52
|
+
def default_options
|
53
|
+
{
|
54
|
+
plugin_class: self
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Upon class load (or whenever you call this) register the core configuration
|
59
|
+
# for the pluggable class.
|
60
|
+
#
|
61
|
+
# param options [Any] a list of keyword options to be passed in to customize registration.
|
62
|
+
#
|
63
|
+
# @return [Void]
|
64
|
+
sig(:final) { params(options: T.untyped).void }
|
65
|
+
def register_plugin(**options)
|
66
|
+
options[:plugin_class].config_context[:required_instance_methods].each do |req_method|
|
67
|
+
|
68
|
+
unless options[:required_instance_methods].key?(req_method)
|
69
|
+
raise Setsuzoku::Exception::UndefinedRequiredMethod.new(registering_instance: self, plugin_class: options[:plugin_class], method_name: req_method)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
self.plugin_context = self.default_options.merge(options)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def assign_plugin(*_args, &block)
|
78
|
+
self.plugin = self.class.plugin_context[:plugin_class].new(registering_instance: self, **self.class.plugin_context.except(:plugin_class))
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define the Plugin that this class should use for its plugin interface.
|
82
|
+
#
|
83
|
+
# @return [Class] any class in Setsuzoku::Plugin
|
84
|
+
sig { abstract.returns(T.untyped) }
|
85
|
+
def plugin_class; end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Setsuzoku
|
5
|
+
# The base definition for a plugin.
|
6
|
+
# A plugin is a unification of an ApiStrategy and an AuthStrategy.
|
7
|
+
# It allows each Strategy to manage its various jobs of sending/receiving requests, and managing authentication/connections.
|
8
|
+
# However it acts as the director that allows these 2 to interact with one another.
|
9
|
+
module Plugin
|
10
|
+
autoload :Mobile, 'setsuzoku/plugin/mobile'
|
11
|
+
|
12
|
+
extend Forwardable
|
13
|
+
extend T::Sig
|
14
|
+
extend T::Helpers
|
15
|
+
abstract!
|
16
|
+
|
17
|
+
attr_accessor :name
|
18
|
+
attr_accessor :registered_instance
|
19
|
+
attr_accessor :service
|
20
|
+
attr_accessor :config_context
|
21
|
+
def_delegators :@service, :auth_strategy, :api_strategy, :exception_handler, :call_external_api, :request_class, :new_credential!
|
22
|
+
|
23
|
+
alias :plugin_service :service
|
24
|
+
alias :plugin_request_class :request_class
|
25
|
+
|
26
|
+
AVAILABLE_SERVICES = {
|
27
|
+
web_service: Setsuzoku::Service::WebService::Service
|
28
|
+
}
|
29
|
+
|
30
|
+
def self.included(klass)
|
31
|
+
klass.extend(ClassMethods)
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
def config_context
|
36
|
+
@config_context
|
37
|
+
end
|
38
|
+
|
39
|
+
def config_context=(val)
|
40
|
+
@config_context = val
|
41
|
+
end
|
42
|
+
# Register the service for a plugin.
|
43
|
+
# This will collect configuration level data to use when a plugin is registered.
|
44
|
+
# It will also impose interface requirements on the registering class.
|
45
|
+
#
|
46
|
+
# @param name [String] the name of the plugin.
|
47
|
+
# @param options [Any] list of keyword options to customize service registration.
|
48
|
+
#
|
49
|
+
# @return [void]
|
50
|
+
def register(name:, service:, **options)
|
51
|
+
context = {
|
52
|
+
name: name,
|
53
|
+
service: service
|
54
|
+
}
|
55
|
+
|
56
|
+
# @options are any additional options you want available inside the plugin
|
57
|
+
|
58
|
+
options.each do |key, val|
|
59
|
+
context[key] = val || self.with_options[key]
|
60
|
+
end
|
61
|
+
|
62
|
+
required_instance_methods = options[:required_instance_methods] || []
|
63
|
+
service[:strategies].each do |type, strategy|
|
64
|
+
type = type.to_s.split('_strategy').first.to_sym
|
65
|
+
strategy = strategy[:strategy]
|
66
|
+
strategy_klass = AVAILABLE_SERVICES[service[:type]].available_strategies[type][strategy]
|
67
|
+
required_instance_methods += strategy_klass.required_instance_methods
|
68
|
+
include strategy_klass.superclass::InterfaceMethods
|
69
|
+
end
|
70
|
+
|
71
|
+
context[:required_instance_methods] = required_instance_methods
|
72
|
+
|
73
|
+
self.config_context = context
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Initialize the plugin.
|
78
|
+
#
|
79
|
+
# @param registering_instance [Any] an instance of a class that needs to register with a plugin.
|
80
|
+
#
|
81
|
+
# @return the new registered instance of a plugin.
|
82
|
+
sig(:final) { params(options: T.untyped).returns(T.untyped) }
|
83
|
+
def initialize(**options)
|
84
|
+
context = self.class.config_context || { name: 'Default plugin', service: {} }
|
85
|
+
self.name = context[:name]
|
86
|
+
service_config = context[:service].except(:type).merge({ plugin: self, credential: options[:credential] })
|
87
|
+
self.registered_instance = options[:registering_instance]
|
88
|
+
if context[:service] && context[:service][:type]
|
89
|
+
service = AVAILABLE_SERVICES[context[:service][:type]]
|
90
|
+
self.service = service.new(service_config)
|
91
|
+
end
|
92
|
+
self.config_context = context.merge(options.except(:registering_instance))
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
# This method allows a safe way to access the registered_instance's methods or specify a static value
|
97
|
+
# This allows allows plugins to be tested in isolation, e.g. independent of a class that uses them.
|
98
|
+
#
|
99
|
+
# @param method_name [Symbol] the name of the method to try or to stub.
|
100
|
+
# @param args [Array] any additional args that need to be passed to the dynamic method.
|
101
|
+
#
|
102
|
+
# @return [Any] any value that the method could return.
|
103
|
+
sig(:final) { params(method_name: Symbol, args: T.untyped).returns(T.untyped) }
|
104
|
+
def get_from_registered_instance_method(method_name, *args)
|
105
|
+
if self.registered_instance
|
106
|
+
#either get the value if its defined generically in the
|
107
|
+
val = self.config_context[:required_instance_methods][method_name.to_sym]
|
108
|
+
self.get_registered_instance_val(val, *args)
|
109
|
+
else
|
110
|
+
#TODO: this needs to return any data type somehow...the plugin might need to stub this, as it stubs tests as well...
|
111
|
+
# this seems like a reasonable approach...
|
112
|
+
"stubbed_#{method_name}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Convenience method for performing instance_exec on the registered_instance.
|
117
|
+
# This is used for calling procs that are defined in the application's registration procs.
|
118
|
+
#
|
119
|
+
# @param val [Any] the value or a proc to instance_exec to get the value.
|
120
|
+
# @param args [Array] a list of args to be used for the proc.
|
121
|
+
#
|
122
|
+
# @return [Any] returns the value of the proc.
|
123
|
+
sig(:final) { params(val: T.untyped, args: T.untyped).returns(T.untyped) }
|
124
|
+
def get_registered_instance_val(val, *args)
|
125
|
+
val.is_a?(Proc) ? self.registered_instance.instance_exec(*args, &val) : val
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
require 'active_support/core_ext/class/subclasses'
|
4
|
+
require 'active_support/inflector/methods'
|
5
|
+
module Setsuzoku
|
6
|
+
module DynamicSpecHelper
|
7
|
+
|
8
|
+
### EXTERNAL PLUGIN SPEC HELPERS ###
|
9
|
+
#
|
10
|
+
# These specs are designed to test and ensure that a plugin's implementations uphold their contract.
|
11
|
+
# Additionally they ensure that a plugin's blackbox stubbing also upholds their contract.
|
12
|
+
#
|
13
|
+
#
|
14
|
+
# This will register plugin specs for a base plugin class that defines its interfaces.
|
15
|
+
#
|
16
|
+
# It will register specs to ensure the base plugin's black box stubs are correct.
|
17
|
+
#
|
18
|
+
# It will also register specs for all implementations of the plugin to ensure the implementations are valid.
|
19
|
+
# (This is similar to register_plugin_tests_and_stubs)
|
20
|
+
def register_all_plugin_tests_and_stubs(plugin_class: self.described_class, subclasses_to_register: plugin_class.subclasses, registering_instance: nil, stub_directory: nil)
|
21
|
+
context "*Dynamic Specs* for #{plugin_class.name}: Stub Definitions" do
|
22
|
+
let(:plugin){ plugin_class.new }
|
23
|
+
plugin_class.spec_definitions.each do |action_name, spec|
|
24
|
+
define_stub_plugin_method(plugin_class, action_name, spec)
|
25
|
+
it(spec[:name], action_name: action_name, &spec[:spec])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
perform_register(plugin_class, registering_instance: registering_instance, subclasses_to_register: subclasses_to_register, stub_directory: stub_directory)
|
30
|
+
end
|
31
|
+
#
|
32
|
+
# This will register plugin specs for a single plugin class.
|
33
|
+
# The specs will test the plugin to ensure its implementations are valid.
|
34
|
+
def register_plugin_tests_and_stubs(plugin_class, registering_instance: nil, stub_directory: nil)
|
35
|
+
perform_register(plugin_class, registering_instance: registering_instance, stub_directory: stub_directory)
|
36
|
+
end
|
37
|
+
#
|
38
|
+
### END EXTERNAL PLUGIN SPEC HELPERS ###
|
39
|
+
|
40
|
+
|
41
|
+
### APPLICATION SPEC HELPERS ###
|
42
|
+
#
|
43
|
+
# These stubs completely black box the plugin's functionality and returns a stubbed response
|
44
|
+
# without invoking the plugin's method.
|
45
|
+
#
|
46
|
+
# Stub all plugin interface methods at a context level for an application class that registers a plugin.
|
47
|
+
# This also defines a before each to execute the stubs.
|
48
|
+
#
|
49
|
+
# @param registering_class [Array/Class] the application's class/es that register a plugin and need methods stubbed.
|
50
|
+
#
|
51
|
+
# @return void
|
52
|
+
def stub_all_plugin_methods(registering_class = self.described_class)
|
53
|
+
registering_class = [registering_class] unless registering_class.is_a?(Array)
|
54
|
+
registering_class.each do |klass|
|
55
|
+
plugin_class = klass.plugin_context[:plugin_class]
|
56
|
+
plugin_class.spec_definitions.each do |action_name, spec|
|
57
|
+
define_stub_plugin_method(plugin_class, action_name, spec, true)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
#
|
62
|
+
# Stub all plugin interface methods for an application class that registers a plugin for a single spec.
|
63
|
+
#
|
64
|
+
# @param registering_class [Array/Class] the application's class/es that register a plugin and need methods stubbed.
|
65
|
+
#
|
66
|
+
# @return void
|
67
|
+
def stub_plugin_methods_for_spec(registering_class = self.described_class)
|
68
|
+
plugin_class = registering_class.plugin_context[:plugin_class]
|
69
|
+
plugin_class.spec_definitions.each do |action_name, spec|
|
70
|
+
allow_any_instance_of(plugin_class).to receive(action_name).and_return(spec[:stub][:response])
|
71
|
+
# a plugin spec might have some dynamic data, e.g. certain methods are called with plugin specific args.
|
72
|
+
if spec[:stub][:dynamic_methods]
|
73
|
+
spec[:stub][:dynamic_methods].each do |meth, resp|
|
74
|
+
#how can we ignore the call count instead of just putting a high limit?
|
75
|
+
allow_any_instance_of(plugin_class).to receive(meth).and_return(resp)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
#
|
81
|
+
### END APPLICATION SPEC HELPERS ###
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def perform_register(plugin_class, register_tests = true, registering_instance: nil, subclasses_to_register: nil, stub_directory: nil)
|
86
|
+
#iterate over all api methods defined by the plugin and create a context and tests for each method
|
87
|
+
# in register_tests_and_get_stubs, and return the stub defs up to this method.
|
88
|
+
plugin_classes = subclasses_to_register || [plugin_class]
|
89
|
+
plugin_classes.each do |plugin_class|
|
90
|
+
p = plugin_class.new(registering_instance: registering_instance)
|
91
|
+
register_tests_and_get_stubs(p, register_tests).each do |method_name, stubs|
|
92
|
+
register_stub(p, method_name, stubs, registering_instance: registering_instance, stub_directory: stub_directory)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# This will iterate over every registered plugin defined in the including helper
|
98
|
+
# and generate formatted hashes of stub configs.
|
99
|
+
#
|
100
|
+
# register_stubs uses these config objects to define stubs dynamically
|
101
|
+
def register_tests_and_get_stubs(plugin, register_tests = true)
|
102
|
+
definitions = {}
|
103
|
+
specs = []
|
104
|
+
action_name_namespace = :"#{plugin.name.gsub(' ', '').underscore}"
|
105
|
+
stub_name = :"stub_#{action_name_namespace}"
|
106
|
+
definitions[stub_name] = { action_name_namespace => {} }
|
107
|
+
plugin.api_actions.each do |api_action|
|
108
|
+
action_name, options = api_action
|
109
|
+
plugin.api_strategy.current_action = action_name
|
110
|
+
request_props = plugin.api_strategy.get_request_properties(action_name: action_name, for_stub: true)
|
111
|
+
(definitions[stub_name][action_name_namespace][action_name] ||= []) << {
|
112
|
+
method: request_props[:request_method],
|
113
|
+
request_format: request_props[:request_format],
|
114
|
+
response_format: request_props[:response_format],
|
115
|
+
url: request_props[:formatted_full_url],
|
116
|
+
stub_data: request_props[:stub_data]
|
117
|
+
}
|
118
|
+
# include the generic interface specs for this plugin's action
|
119
|
+
specs << { spec: plugin.class.spec_definitions[action_name], action_name: action_name }
|
120
|
+
end
|
121
|
+
if register_tests
|
122
|
+
context "*Dynamic Specs* for #{plugin.class.name}: Interface Implementations" do
|
123
|
+
#we will instantiate the plugin, any registered_instance dependent methods/data must be stubbed by the plugin because of this!
|
124
|
+
let!(:plugin) { plugin }
|
125
|
+
|
126
|
+
before :each do
|
127
|
+
send(stub_name, self)
|
128
|
+
end
|
129
|
+
|
130
|
+
specs.each do |spec_def|
|
131
|
+
if spec_def[:spec] && spec_def[:spec][:spec]
|
132
|
+
it(spec_def[:spec][:name], &spec_def[:spec][:spec])
|
133
|
+
else
|
134
|
+
it "Undefined spec for #{spec_def[:action_name]}" do
|
135
|
+
raise NotImplemented, "Spec for action: #{spec_def[:action_name]} is not defined. Expected #{plugin.class} or its parent to define it."
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
definitions
|
143
|
+
end
|
144
|
+
|
145
|
+
# This method will then iterate over stub defs and actually define the stubs here in the stub_defs array.
|
146
|
+
# It will then lastly collect all the stubs in stub_defs array and do call to define the stub.
|
147
|
+
def register_stub(plugin, method_name, stubs, registering_instance: nil, stub_directory: nil)
|
148
|
+
stub_defs = []
|
149
|
+
stubs.each do |provider, stub|
|
150
|
+
stub.each do |action_name, props|
|
151
|
+
props.each do |propz|
|
152
|
+
plugin.api_strategy.current_action = action_name
|
153
|
+
default_headers = plugin.api_strategy.request_options(propz[:request_format])[:headers] || { 'Authorization' => 'Bearer stubbed_token', 'Content-Type' => "application/#{propz[:request_format]}" }
|
154
|
+
if plugin.auth_strategy.is_a?(Setsuzoku::Service::WebService::AuthStrategies::BasicAuthStrategy)
|
155
|
+
basic_auth = plugin.api_strategy.request_options(propz[:request_format])[:basic_auth]
|
156
|
+
auth_header = { 'Authorization' => "Basic #{Base64.encode64("#{basic_auth[:username]}:#{basic_auth[:password]}").gsub("\n", '')}" }
|
157
|
+
default_headers.merge!(auth_header)
|
158
|
+
end
|
159
|
+
headers = propz[:headers] ? [propz[:headers]] : [default_headers]
|
160
|
+
stub_directory ||= "#{plugin.class.plugin_namespace}/#{provider}"
|
161
|
+
headers.each do |header|
|
162
|
+
header = {
|
163
|
+
'Accept'=>'*/*',
|
164
|
+
'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
165
|
+
'User-Agent'=>'Ruby'
|
166
|
+
}.merge(header.deep_stringify_keys)
|
167
|
+
|
168
|
+
if propz[:stub_data]
|
169
|
+
# we have specified specific stub data for this action
|
170
|
+
propz[:stub_data].each do |stub_datum|
|
171
|
+
url, body = get_url_and_body(plugin, propz, stub_directory, action_name, stub_datum[:url_params])
|
172
|
+
|
173
|
+
stub_defs << proc do
|
174
|
+
stub_request(propz[:method], url)
|
175
|
+
.with(
|
176
|
+
headers: header, body: body, query: stub_datum[:url_params]
|
177
|
+
)
|
178
|
+
.to_return(status: 200, headers: {}, body: File.read("#{Dir.pwd}/spec/support/setsuzoku/#{plugin.class.plugin_namespace}/#{provider}/#{action_name}.#{propz[:response_format]}"))
|
179
|
+
end
|
180
|
+
end
|
181
|
+
else
|
182
|
+
# this is a generic action so the stub uses the default structure
|
183
|
+
url, body = get_url_and_body(plugin, propz, stub_directory, action_name)
|
184
|
+
|
185
|
+
stub_defs << proc do
|
186
|
+
stub_request(propz[:method], url)
|
187
|
+
.with(
|
188
|
+
headers: header, body: body
|
189
|
+
)
|
190
|
+
.to_return(status: 200, headers: {}, body: File.read("#{Dir.pwd}/spec/support/setsuzoku/#{plugin.class.plugin_namespace}/#{provider}/#{action_name}.#{propz[:response_format]}"))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
#iterate over all the stub_requests collected above and call them to stub them.
|
199
|
+
define_method method_name do |spec_instance = self|
|
200
|
+
stub_defs.each do |stub_def|
|
201
|
+
instance_exec(&stub_def)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def define_stub_plugin_method(plugin_class, action_name, spec, always_stub = false)
|
207
|
+
stub_plugin_method_name = :"stub_plugin_method_#{action_name}"
|
208
|
+
define_method stub_plugin_method_name do
|
209
|
+
#TODO: let this use the appropriate rspec instance stub method
|
210
|
+
#TODO: Does it make sense to have the stub with the method? or should it be in a json file we parse?
|
211
|
+
plugin_class.any_instance.should_receive(action_name).and_return(spec[:stub][:response])
|
212
|
+
# a plugin spec might have some dynamic data, e.g. certain methods are called with plugin specific args.
|
213
|
+
if spec[:stub][:dynamic_methods]
|
214
|
+
spec[:stub][:dynamic_methods].each do |meth, resp|
|
215
|
+
#how can we ignore the call count instead of just putting a high limit?
|
216
|
+
plugin_class.any_instance.should_receive(meth).and_return(resp)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
if always_stub
|
221
|
+
before :each do
|
222
|
+
send(stub_plugin_method_name)
|
223
|
+
end
|
224
|
+
else
|
225
|
+
before :each, action_name: action_name do
|
226
|
+
send(stub_plugin_method_name)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def get_url_and_body(plugin, propz, stub_directory, action_name, url_params = nil)
|
232
|
+
body = nil
|
233
|
+
url = propz[:url]
|
234
|
+
format = propz[:request_format].to_s.include?('x-www-form') ? :json : propz[:request_format]
|
235
|
+
begin
|
236
|
+
body = File.read("#{Dir.pwd}/spec/support/setsuzoku/#{stub_directory}/#{action_name}_request.#{format}")
|
237
|
+
body.squish!
|
238
|
+
case format
|
239
|
+
when :xml
|
240
|
+
body.gsub!('> <', '><')
|
241
|
+
when :json
|
242
|
+
body.gsub!(' "', '"')
|
243
|
+
body.gsub!('" ', '"')
|
244
|
+
body.gsub!(': ', ':')
|
245
|
+
body.gsub!('{ ', '{')
|
246
|
+
body.gsub!(' }', '}')
|
247
|
+
end
|
248
|
+
|
249
|
+
# We will convert these to url params when making requests. need to replicate that.
|
250
|
+
|
251
|
+
|
252
|
+
req_params = case format
|
253
|
+
when :json
|
254
|
+
temp_body = JSON.parse(body)
|
255
|
+
if url_params
|
256
|
+
url_params.each{ |k,v| temp_body.delete(k.to_s) }
|
257
|
+
end
|
258
|
+
temp_body
|
259
|
+
else
|
260
|
+
# will we need to parse xml here?
|
261
|
+
req_params = {}
|
262
|
+
body
|
263
|
+
end
|
264
|
+
|
265
|
+
|
266
|
+
if url.match?(/.*\{\{.*\}\}/)
|
267
|
+
url, body = plugin.api_strategy.replace_dynamic_vars(full_url: url, req_params: req_params)
|
268
|
+
end
|
269
|
+
|
270
|
+
case format
|
271
|
+
when :json
|
272
|
+
#faraday sorts hash keys for some reason
|
273
|
+
body = JSON.parse(body).sort.to_h
|
274
|
+
end
|
275
|
+
[url, body]
|
276
|
+
rescue
|
277
|
+
[url, nil]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|