diff --git a/.gitignore b/.gitignore index e483dff..73b0073 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ + # rcov generated coverage coverage.data @@ -15,7 +16,7 @@ doc # jeweler generated pkg -# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: +# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: # # * Create a file at ~/.gitignore # * Include files you want ignored diff --git a/README.md b/README.md index ad2c698..f0a5147 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ gem install blockscore If you are using Rails, add the following to your `Gemfile`: ```ruby -gem 'blockscore', '~> 4.1.2' +gem 'blockscore', '~> 4.2.0' ``` ## Getting Started @@ -60,5 +60,5 @@ To see the list of calls you can make, please visit our [full Ruby API reference The test suite uses a public BlockScore API key that was created specifically to ease the testing and contribution processes. **Please do not enter personal details for tests.** In order to run the test suite: ```shell -$ rake test +$ rspec spec ``` diff --git a/blockscore.gemspec b/blockscore.gemspec index 7425d4c..0754300 100644 --- a/blockscore.gemspec +++ b/blockscore.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 1.0' spec.add_development_dependency 'simplecov', '~> 0' spec.add_development_dependency 'rspec', '~> 3' + spec.add_development_dependency 'rspec-its', '~> 1' spec.add_development_dependency 'webmock', '~> 1.21' spec.add_development_dependency 'faker', '~> 1.4' spec.add_development_dependency 'factory_girl', '~> 4.5' diff --git a/lib/blockscore.rb b/lib/blockscore.rb index b652aa5..71e547a 100644 --- a/lib/blockscore.rb +++ b/lib/blockscore.rb @@ -1,3 +1,4 @@ +require 'delegate' require 'forwardable' require 'httparty' require 'json' @@ -26,6 +27,7 @@ require 'blockscore/watchlist_hit' require 'blockscore/collection' +require 'blockscore/collection/member' require 'blockscore/connection' require 'blockscore/dispatch' require 'blockscore/fingerprint' @@ -34,11 +36,7 @@ require 'blockscore/version' module BlockScore - def self.api_key=(api_key) - @api_key = api_key - end - - def self.api_key - @api_key + class << self + attr_accessor :api_key end end diff --git a/lib/blockscore/actions/retrieve.rb b/lib/blockscore/actions/retrieve.rb index e0585af..87725d7 100644 --- a/lib/blockscore/actions/retrieve.rb +++ b/lib/blockscore/actions/retrieve.rb @@ -11,8 +11,10 @@ module Actions # => # module Retrieve module ClassMethods - def retrieve(id) - get("#{endpoint}/#{id}", {}) + def retrieve(id, options = {}) + fail ArgumentError if id.empty? + req = ->() { get("#{endpoint}/#{id}", options) } + new(id: id, &req) end end diff --git a/lib/blockscore/actions/update.rb b/lib/blockscore/actions/update.rb index df9e25e..1de39ed 100644 --- a/lib/blockscore/actions/update.rb +++ b/lib/blockscore/actions/update.rb @@ -28,7 +28,7 @@ module Update def_delegators 'self.class', :endpoint, :patch def save! - if respond_to? :id + if persisted? patch("#{endpoint}/#{id}", filter_params) true else diff --git a/lib/blockscore/base.rb b/lib/blockscore/base.rb index b19d657..b56d3b4 100644 --- a/lib/blockscore/base.rb +++ b/lib/blockscore/base.rb @@ -4,19 +4,37 @@ module BlockScore class Base extend Connection - attr_reader :attributes - - def initialize(options = {}) + def initialize(options = {}, &block) + @loaded = !(block) + @proc = block @attributes = options end + def attributes + return @attributes if @loaded + force! + @attributes + end + + def force! + res = @proc.call + @attributes = res.attributes.merge(@attributes) + @loaded = true + self + end + + def id + @attributes.fetch(:id, nil) + end + def inspect - "#<#{self.class}:0x#{object_id.to_s(16)} JSON: " + JSON.pretty_generate(attributes) + str_attr = "JSON:#{JSON.pretty_generate(attributes)}" + "#<#{self.class}:0x#{object_id.to_s(16)} #{str_attr}>" end def refresh - r = self.class.retrieve(id) - @attributes = r.attributes + res = self.class.retrieve(id) + @attributes = res.attributes true rescue Error @@ -45,16 +63,18 @@ def self.api_url end def self.endpoint - if self == Base - fail NotImplementedError, 'Base is an abstract class, not an API resource' - end + fail NotImplementedError, 'Base is an abstract class, not an API resource' if equal?(Base) "#{api_url}#{Util.to_plural(resource)}" end + def persisted? + !id.nil? + end + protected - def add_accessor(symbol, *args) + def add_accessor(symbol, *_args) singleton_class.instance_eval do define_method(symbol) do wrap_attribute(attributes[symbol]) diff --git a/lib/blockscore/collection.rb b/lib/blockscore/collection.rb index e81284d..b1b4f12 100644 --- a/lib/blockscore/collection.rb +++ b/lib/blockscore/collection.rb @@ -1,18 +1,239 @@ module BlockScore + # Collection is a proxy between the parent and the asssociated members + # where parent is some instance of a resource + # class Collection < Array - def initialize(target) - super() - @target = target + # @!attribute [r] parent + # resource which owns a collection of other resources + # + # @example + # person.question_sets.parent # => person + # + # @return [BlockScore::Base] a resource + # + # @api private + attr_reader :parent + + # Sets parent and member_class then registers embedded ids + # + # @param [BlockScore::Base] parent + # @param [Class] class of collection members + # + # @return [undefined] + # + # @api private + def initialize(parent, member_class) + @parent = parent + @member_class = member_class + register_parent_data + end + + # Syntactic sugar method for returning collection + # + # @example + # all # returns collection + # + # @return [self] + # + # @api public + def all + self + end + + # Initializes new {member_class} with `params` + # + # - Ensures a parent id is meged into `params` (see #default_params). + # - Defines method `#save` on new collection member + # - Adds new item to collection + # + # @example usage + # + # >> person = person = BlockScore::Person.retrieve('55de4af7643735000300000f') + # >> person.question_sets.new + # => # + # + # @param params [Hash] initial params for member + # + # @return instance of {member_class} + # + # @api public + def new(params = {}) + attributes = params.merge(default_params) + instance = member_class.new(attributes) + + new_member(instance) do |member| + self << member + end + end + + # Relaod the contents of the collection + # + # @example usage + # person.question_sets.refresh # => [# 'person' + # + # @return [String] + # + # @api semipublic + def parent_name + parent.class.resource + end + + # Initialize a collection member and save it + # + # @example + # >> person.question_sets.create + # => # instance of QuestionSet + # + # @param id [String] resource id + # + # @return instance of {member_class} if found + # @raise [BlockScore::NotFoundError] otherwise + # + # @api public + def retrieve(id) + each do |item| + next unless item.id == id + return item + end + + instance = member_class.retrieve(id) + + new_member(instance) do |member| + register_to_parent(member) + end + end + + protected + + # @!attribute [r] member_class + # class which will be used for the embedded + # resources in the collection + # + # @return [Class] + # + # @api private + attr_reader :member_class + + # Default params for making an instance of {member_class} + # + # @return [Hash] + # + # @api private + def default_params + { + foriegn_key => parent.id + } end private - def method_missing(method, *args, &block) - @target.public_send(method, *args, &block) + # Generate foriegn key name for parent resource + # + # @return [Symbol] resource name as id + # + # @api private + def foriegn_key + :"#{parent_name}_id" + end + + # Initialize a new collection member + # + # @param instance [BlockScore::Base] collection member instance + # @yield [Member] initialized member + # + # @return [Member] new member + # + # @api private + def new_member(instance, &blk) + Member.new(parent, instance).tap(&blk) + end + + # Check if `parent_id` is defined on `item` + # + # @param item [BlockScore::Base] any resource + # + # @return [Boolean] + # + # @api private + def parent_id?(item) + parent.id && item.send(foriegn_key) == parent.id + end + + # Register a resource in collection + # + # @param item [BlockScore::Base] a resource + # + # @raise [BlockScore::Error] if no `parent_id` + # @return [BlockScore::Base] otherwise + # + # @api private + def register_to_parent(item) + fail Error, 'None belonging' unless parent_id?(item) + ids << item.id + self << item + item + end + + # Fetches embedded ids from parent and adds to self + # + # @return [undefined] + # + # @api private + def register_parent_data + ids.each do |id| + item = member_class.retrieve(id) + self << item + end + end + + # ids that belong to the collection + # + # @return [Array] + # + # @api private + def ids + parent.attributes.fetch(:"#{Util.to_plural(member_class.resource)}", []) end end end diff --git a/lib/blockscore/collection/member.rb b/lib/blockscore/collection/member.rb new file mode 100644 index 0000000..ff50e08 --- /dev/null +++ b/lib/blockscore/collection/member.rb @@ -0,0 +1,100 @@ +module BlockScore + class Collection + # Member of a {Collection} class + class Member < SimpleDelegator + # Initialize a new member + # + # @param parent [BlockScore::Base] parent resource + # @param instance [BlockScore::Base] member instance + # + # @return [undefined] + # + # @api private + def initialize(parent, instance) + @instance = instance + @parent = parent + + super(instance) + end + + # Save parent, set parent id, and save instance + # + # @example + # # saves both unsaved person and unsaved question_set + # person = Person.new(attributes) + # question_set = QuestionSet.new + # Member.new(person, question_set).save + # + # @return return value of instance `#save` + # + # @api public + def save + save_parent + send(:"#{parent_name}_id=", parent.id) + result = instance.save + ids.push(instance.id) unless ids.include?(instance.id) + result + end + + private + + # Name of parent resource + # + # @example + # self.parent_name # => 'person' + # + # @return [String] + # + # @api private + def parent_name + parent.class.resource + end + + # Save parent if it hasn't already been saved + # + # @return return of parent.save if previously unsaved + # @return nil otherwise + # + # @api private + def save_parent + parent.save unless parent_saved? + end + + # Check if parent is saved + # + # @return [Boolean] + # + # @api private + def parent_saved? + parent.id + end + + # @!attribute [r] instance + # member instance methods are delegated to + # + # @return [BlockScore::Base] + # + # @api private + attr_reader :instance + + # @!attribute [r] parent + # collection parent the collectino conditionally updates + # + # @return [BlockScore::Base] + # + # @api private + attr_reader :parent + + private + + # ids that belong to associated parent resource + # + # @return [Array] + # + # @api private + def ids + parent.attributes.fetch(:"#{Util.to_plural(instance.class.resource)}", []) + end + end + end +end diff --git a/lib/blockscore/connection.rb b/lib/blockscore/connection.rb index ade3d24..9d1c5df 100644 --- a/lib/blockscore/connection.rb +++ b/lib/blockscore/connection.rb @@ -32,7 +32,7 @@ def request(method, path, params) begin response = execute_request(method, path, params) rescue SocketError, Errno::ECONNREFUSED => e - fail APIConnectionError, e.message + raise APIConnectionError, e.message end Response.handle_response(resource, response) @@ -55,7 +55,7 @@ def execute_request(method, path, params) def encode_path_params(path, params) encoded = URI.encode_www_form(params) - [path, encoded].join("?") + [path, encoded].join('?') end end end diff --git a/lib/blockscore/errors/api_error.rb b/lib/blockscore/errors/api_error.rb index 1b9bd6b..43a6d0d 100644 --- a/lib/blockscore/errors/api_error.rb +++ b/lib/blockscore/errors/api_error.rb @@ -15,7 +15,7 @@ class APIError < Error # APIError - Indicates an error on the server side (HTTP 5xx) # AuthenticationError - Indicates an authentication error (HTTP 401) def initialize(response) - body = JSON.parse(response.body, :symbolize_names => true) + body = JSON.parse(response.body, symbolize_names: true) @message = body[:error][:message] @http_status = response.code @@ -24,8 +24,8 @@ def initialize(response) end def to_s - status_string = @http_status ? "(Status: #{@http_status})" : "" - type_string = @error_type ? "(Type: #{@error_type})" : "" + status_string = @http_status ? "(Status: #{@http_status})" : '' + type_string = @error_type ? "(Type: #{@error_type})" : '' "#{type_string} #{@message} #{status_string}" end diff --git a/lib/blockscore/errors/invalid_request_error.rb b/lib/blockscore/errors/invalid_request_error.rb index f76873e..e7ff856 100644 --- a/lib/blockscore/errors/invalid_request_error.rb +++ b/lib/blockscore/errors/invalid_request_error.rb @@ -19,9 +19,9 @@ def initialize(response) end def to_s - status_string = @http_status ? "(Status: #{@http_status})" : "" - type_string = @error_type ? "(Type: #{@error_type})" : "" - param_string = @param ? "(#{@param})" : "" + status_string = @http_status ? "(Status: #{@http_status})" : '' + type_string = @error_type ? "(Type: #{@error_type})" : '' + param_string = @param ? "(#{@param})" : '' "#{type_string} #{@message} #{param_string} #{status_string}" end diff --git a/lib/blockscore/person.rb b/lib/blockscore/person.rb index ec311a7..b26e3a4 100644 --- a/lib/blockscore/person.rb +++ b/lib/blockscore/person.rb @@ -11,7 +11,7 @@ class Person < Base def initialize(options = {}) super - @question_sets = Collection.new(QuestionSet.new(person: self)) + @question_sets = Collection.new(self, QuestionSet) end def valid? diff --git a/lib/blockscore/question_set.rb b/lib/blockscore/question_set.rb index 944230d..c79ab0c 100644 --- a/lib/blockscore/question_set.rb +++ b/lib/blockscore/question_set.rb @@ -6,14 +6,7 @@ class QuestionSet < Base include BlockScore::Actions::Retrieve include BlockScore::Actions::All - def_delegators 'self.class', :retrieve, :all, :post, :endpoint - - def create - result = self.class.create(person_id: person.id) - person.question_sets << result.id - - result - end + def_delegators 'self.class', :post, :endpoint def score(answers = nil) if answers.nil? && attributes diff --git a/lib/blockscore/util.rb b/lib/blockscore/util.rb index 3624aeb..6c5cf6e 100644 --- a/lib/blockscore/util.rb +++ b/lib/blockscore/util.rb @@ -11,13 +11,13 @@ module Util } def parse_json!(json_obj) - JSON.parse(json_obj, :symbolize_names => true) + JSON.parse(json_obj, symbolize_names: true) end def parse_json(json_obj) parse_json! json_obj rescue JSON::ParserError - fail Error, "An error has occurred. If this problem persists, please message support@blockscore.com." + raise Error, 'An error has occurred. If this problem persists, please message support@blockscore.com.' end def create_object(resource, options = {}) @@ -65,16 +65,16 @@ def to_constant(camel_cased_word) end def to_camelcase(str) - str.split('_').map { |i| i.capitalize }.join('') + str.split('_').map(&:capitalize).join('') end # Taken from Rulers: http://git.io/vkWqf def to_underscore(str) - str.gsub(/::/, '/'). - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). - tr("-", "_"). - downcase + str.gsub(/::/, '/') + .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + .gsub(/([a-z\d])([A-Z])/, '\1_\2') + .tr('-', '_') + .downcase end end end diff --git a/lib/blockscore/version.rb b/lib/blockscore/version.rb index 26e1366..5f17869 100644 --- a/lib/blockscore/version.rb +++ b/lib/blockscore/version.rb @@ -1,3 +1,3 @@ module BlockScore - VERSION = '4.1.2'.freeze -end \ No newline at end of file + VERSION = '4.2.0'.freeze +end diff --git a/spec/factories.rb b/spec/factories.rb index 77209c3..c5d8b45 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -9,10 +9,10 @@ def initialize delegate :association, to: :@strategy - def result(evaluation) + def result(evaluation, attrs = {}) compiled = @strategy.result(evaluation) case compiled - when BlockScore::Base then compiled.attributes.to_json + when BlockScore::Base then compiled.attributes.merge(attrs).to_json when Hash then compiled.to_json else fail ArgumentError, "don't know how to handle type #{evaluation.class.inspect}" @@ -38,6 +38,10 @@ def full_address "#{street} #{city} #{country}" end +def resource_id + Faker::Number.hexadecimal(24) +end + FactoryGirl.define do # Each response has this metadata so we define it as a trait trait :metadata do @@ -175,6 +179,29 @@ def full_address details { build(:company_details) } end + factory :fake_member, class: 'BlockScore::FakeResource' do + object { 'fake_member' } + transient do + given_id resource_id + parent_id resource_id + end + + id { given_id } + fake_resource_id { parent_id } + end + + factory :fake_resource, class: 'BlockScore::FakeResource' do + object { 'fake_resource' } + metadata + transient { members_count 2 } + + fake_resources do + members_count.times.map do + resource_id + end + end + end + factory :person_params, class: 'BlockScore::Person' do name document @@ -186,6 +213,7 @@ def full_address factory :person, class: 'BlockScore::Person' do skip_create + transient { question_sets_count 1 } object { 'person' } metadata @@ -199,15 +227,20 @@ def full_address address document details { build(:person_details) } + question_sets do - rand(0..5).times.collect { Faker::Base.regexify(/\d{24}/) } + question_sets_count.times.map do + resource_id + end end + + initialize_with { new(attributes) } end # QuestionSet Factory factory :question_set_params, class: 'BlockScore::QuestionSet' do - person_id { Faker::Base.regexify(/\d{24}/) } + person_id { resource_id } end factory :question_set, class: 'BlockScore::QuestionSet' do @@ -217,7 +250,7 @@ def full_address metadata timestamps testmode - person_id { Faker::Base.regexify(/\d{24}/) } + person_id { resource_id } score { rand * 100 } expired { false } time_limit { rand(120..360) } @@ -303,7 +336,7 @@ def full_address # We can do this because the error type is determined by the # HTTP response code. factory :blockscore_error, class: Hash, traits: [:resource] do - ignore do + transient do error_type 'api_error' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cab1adf..fbfd78a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ require 'webmock' require 'webmock/rspec' require 'rspec' +require 'rspec/its' BlockScore::Spec.setup diff --git a/spec/support/stubbed_request.rb b/spec/support/stubbed_request.rb index ee29519..4f1428d 100644 --- a/spec/support/stubbed_request.rb +++ b/spec/support/stubbed_request.rb @@ -5,6 +5,12 @@ def initialize(request) @uri = request.uri end + def body + JSON.parse(request.body) + rescue + nil + end + def factory_name resource.singularize end diff --git a/spec/support/stubbed_response.rb b/spec/support/stubbed_response.rb index 61fca05..9bce4dc 100644 --- a/spec/support/stubbed_response.rb +++ b/spec/support/stubbed_response.rb @@ -94,7 +94,7 @@ def response private def factory_response - json(factory_name) + json(factory_name, request.body) end end diff --git a/spec/unit/blockscore/actions_spec.rb b/spec/unit/blockscore/actions_spec.rb index 23d63d7..c3aa3c7 100644 --- a/spec/unit/blockscore/actions_spec.rb +++ b/spec/unit/blockscore/actions_spec.rb @@ -42,8 +42,18 @@ module BlockScore let(:route) { 'https://api.blockscore.com/fake_resources/abc123' } it 'uses the correct endpoint' do - should receive(:request).with(:get, route, {}).once - mock.retrieve('abc123') + should receive(:request).with(:get, route, {}).once { resource } + mock.retrieve('abc123').attributes + end + + context 'when id is invalid' do + pending 'should raise not found if not a resource style id' do + expect { mock.retrieve('bad_id') }.to raise_error(NotFoundError) + end + + it 'should raise ArgumentError if empty' do + expect { mock.retrieve('') }.to raise_error(ArgumentError) + end end end diff --git a/spec/unit/blockscore/candidate_spec.rb b/spec/unit/blockscore/candidate_spec.rb index 3ef3a2f..bde5e3a 100644 --- a/spec/unit/blockscore/candidate_spec.rb +++ b/spec/unit/blockscore/candidate_spec.rb @@ -32,7 +32,7 @@ module BlockScore subject(:search) { -> { candidate.search(constraints) } } context 'search request' do - let(:uri) { "/watchlists" } + let(:uri) { '/watchlists' } let(:body) { { 'candidate_id' => candidate.id }.merge(constraints) } let(:expected) { { body: hash_including(body) } } before { search.call } diff --git a/spec/unit/blockscore/collection_spec.rb b/spec/unit/blockscore/collection_spec.rb new file mode 100644 index 0000000..1ed9cd0 --- /dev/null +++ b/spec/unit/blockscore/collection_spec.rb @@ -0,0 +1,178 @@ +module BlockScore + RSpec.describe Collection do + let(:parent) { create(:fake_resource) } + let(:member_class) { FakeResource } + let(:collection) { Collection.new(parent, member_class) } + + before do + Util::PLURAL_LOOKUP['fake_resource'] = 'fake_resources' + allow(member_class).to receive(:create) { create(:fake_member, parent_id: parent.id) } + end + + describe '#all' do + subject { collection.all } + + it 'should return self' do + is_expected.to be(collection) + end + end + + describe '#create' do + let(:created) { create(:fake_member, parent_id: parent.id) } + subject { collection.create } + before(:each) do + allow(member_class).to receive(:create) { created } + end + + it 'should send merged default and arg params' do + foriegn_key = :"#{parent.class.resource}_id" + expect(member_class).to receive(:create).with(foriegn_key => parent.id, foo_param: 'bar_attr') + collection.create(foo_param: 'bar_attr') + end + + it 'should update ids in Parent#attributes' do + expect(parent.attributes[:fake_resources]).to include(subject.id) + end + + it 'should add to collection' do + expect(collection).to include(subject) + end + + it 'should error if parent not created' do + parent = FakeResource.new + collection = Collection.new(parent, member_class) + expect { collection.create }.to raise_error(Error, 'Create parent first') + end + + it 'should retrieve item from registered' do + found_qs = collection.retrieve(subject.id) + expect(subject).to eq(found_qs) + end + end + + describe '#new' do + subject { collection.new } + + it 'should add to collection' do + count = collection.size + result = collection.new + expect(collection.size).to eq(count + 1) + expect(collection).to include(result) + end + + context 'when saving' do + it 'should have parent#id after saving' do + subject.fake_resource_id = 'some_id' + subject.save + expect(subject.fake_resource_id).to eq(parent.id) + end + + it 'should be retrievable after' do + collection.create + subject.save + collection.create + expect(collection.retrieve(subject.id)).to be(subject) + end + end + end + + describe '#retrieve' do + context 'when in person#attributes' do + let(:parent) { create(:fake_resource) } + let(:existing_id) { parent.attributes[:fake_resources].last } + subject { collection.retrieve(existing_id) } + + it 'should retrieve correctly' do + last = collection.last + expect(subject).to eq(last) + expect(subject.id).to eq(existing_id) + end + + it 'checks collect' do + expect(collection).to receive(:each).and_call_original + collection.retrieve(existing_id) + end + end + + context 'when not in person#attributes' do + let(:parent) { create(:fake_resource, members_count: 0) } + let(:collection) { Collection.new(parent, member_class) } + let(:item) { create(:fake_member, parent_id: parent.id) } + let(:data) { parent.attributes.fetch(:fake_resources) } + subject { collection.retrieve(item.id) } + + before(:each) do + allow(member_class).to receive(:retrieve).with(item.id) { item } + end + + it 'uses member_class.retrieve' do + expect(member_class).to receive(:retrieve).with(item.id).and_return(item) + expect(collection.retrieve(item.id)).to eq(item) + end + + it 'registers new question set' do + aggregate_failures('for person and collection') do + expect(collection).to receive(:<<).and_call_original + expect(data).to receive(:<<).with(item.id).and_call_original + end + collection.retrieve(item.id) + end + + it 'errors if not belonging' do + item.fake_resource_id = 'some_not_associated_id' + expect { subject }.to raise_error(Error, 'None belonging') + end + end + end + + describe '#refresh' do + subject { collection } + let(:item) { member_class.create } + before(:each) { parent.attributes[:fake_resources] << item.id } + + # refactor + it 'should register new data from parent' do + parent.attributes[:fake_resources].clear + parent.attributes[:fake_resources].push(item.id) + expect(member_class).to receive(:retrieve).with(item.id).at_least(:twice).and_call_original + subject.refresh + end + + it 'should clear and reload' do + expect(subject).to receive(:clear).and_call_original + expect(subject).to receive(:register_parent_data).and_call_original + result = subject.refresh + expect(subject).to be(result) + end + + context 'when creating' do + it 'should be included in refresh' do + result = subject.create + subject.refresh + expect(subject.map(&:id)).to include(result.id) + end + + it 'should include saved in refresh' do + item = FakeResource.new(fake_resource_id: parent.id) + allow(member_class).to receive(:new) { item } + allow(item).to receive(:save) { create(:fake_member, parent_id: parent.id) } + result = subject.new + result.save + subject.refresh + expect(subject.map(&:id)).to include(result.id) + end + end + + context 'when retrieving belonging' do + it 'should be included in refresh' do + item = create(:fake_resource) + item.parent_id = parent.id + allow(member_class).to receive(:retrieve) { item } + result = subject.retrieve(item.id) + subject.refresh + expect(subject.map(&:id)).to include(result.id) + end + end + end + end +end diff --git a/spec/unit/blockscore/member_spec.rb b/spec/unit/blockscore/member_spec.rb new file mode 100644 index 0000000..3b88b57 --- /dev/null +++ b/spec/unit/blockscore/member_spec.rb @@ -0,0 +1,104 @@ +module BlockScore + RSpec.describe Collection::Member do + let(:parent) { create(:fake_resource) } + let(:member_class) { FakeResource } + let(:collection) { Collection.new(parent, member_class) } + + before do + Util::PLURAL_LOOKUP['fake_resource'] = 'fake_resources' + allow(member_class).to receive(:create) { create(:fake_member, parent_id: parent.id) } + end + + context 'when saving' do + let(:member_class) { FakeResource } + let(:parent) { FakeResource.new } + let(:collection) { Collection.new(parent, member_class) } + subject(:member) { collection.new } + + context 'when parent is not persisted' do + before { allow(parent).to receive(:save).and_call_original } + + it { expect(member.save).to be(true) } + + it do + member.save + expect(parent).to have_received(:save).once + end + + context 'after saving' do + before { member.save } + let(:member_parent_id) { member.fake_resource_id } + + it { is_expected.to be_persisted } + it { expect(parent).to be_persisted } + it { expect(member_parent_id).to eql(parent.id) } + end + end + + it 'should not save persisted parent' do + parent.save + + aggregate_failures('when saving') do + expect(parent).not_to receive(:save) + expect(member.save).to be(true) + end + + aggregate_failures('after saving') do + expect(member.persisted?).to be(true) + expect(parent.persisted?).to be(true) + foriegn_key = :"#{parent.class.resource}_id" + expect(member.send(foriegn_key)).to eq(parent.id) + end + end + + it 'should not add to parent if existing' do + member.save + embedded_resource = :"#{Util.to_plural(parent.class.resource)}" + tracked_ids = parent.send(embedded_resource) + size = tracked_ids.size + member.save + expect(size).to eql(size) + expect(tracked_ids).to include(member.id) + end + end + + context 'when created' do + let(:created) { create(:fake_member, parent_id: parent.id) } + subject { collection.create } + before(:each) do + allow(member_class).to receive(:create) { created } + end + + it 'should have the Parent#id' do + expect(member_class).to receive(:create).with(fake_resource_id: parent.id).and_call_original + subject + end + end + + context 'when instantiating a new' do + subject { collection.new } + + it 'should delegate to member_class' do + # odd bug... test fails when replaced with parent.id + args = { fake_resource_id: collection.parent.id } + expect(member_class).to receive(:new).with(args).and_call_original + collection.new + end + + it 'should save parent if not persisted' do + parent = create(:fake_resource) + parent.id = nil + collection = Collection.new(parent, member_class) + item = collection.new + expect(parent).to receive(:save).and_call_original + item.save + end + + it 'should have Parent#id' do + subject.person_id = 'some_id' + subject.save + expect(subject.fake_resource_id).to eq(collection.parent.id) + end + end + end +end diff --git a/spec/unit/blockscore/person_spec.rb b/spec/unit/blockscore/person_spec.rb index ae72825..a840832 100644 --- a/spec/unit/blockscore/person_spec.rb +++ b/spec/unit/blockscore/person_spec.rb @@ -3,6 +3,30 @@ module BlockScore let(:api_stub) { @api_stub } let(:action) { -> { create(:person).details } } + context 'when person has existing question sets' do + let(:person) { create(:person, question_set_count: 1) } + subject(:question_sets) { person.question_sets } + its(:size) { should eql(1) } + its(:ids) { should be(person.attributes[:question_sets]) } + its(:first) { should be_an_instance_of(QuestionSet) } + + it 'should load attributes' do + expect(question_sets.first.attributes).not_to be(nil) + end + end + + it '#persisted?' do + persisted_person = create(:person) + new_person = Person.new + expect(persisted_person.persisted?).to be(true) + expect(new_person.persisted?).to be(false) + end + + it '#id' do + person = create(:person) + expect(person.id).not_to be(nil) + end + it '#valid?' do person = create(:person, status: 'valid') expect(person.valid?).to eq(true) diff --git a/spec/unit/blockscore/question_set_spec.rb b/spec/unit/blockscore/question_set_spec.rb index d498205..e2feac6 100644 --- a/spec/unit/blockscore/question_set_spec.rb +++ b/spec/unit/blockscore/question_set_spec.rb @@ -1,61 +1,41 @@ module BlockScore RSpec.describe QuestionSet do - let(:api_stub) { @api_stub } let(:person) { create(:person_params) } - it 'create' do - person.question_sets.count - person.question_sets.create - assert_requested(api_stub, times: 2) - end - - it 'create question_set count' do - count = person.question_sets.count - person.question_sets.create - expect(count + 1).to be_truthy - assert_requested(api_stub, times: 2) - end - - it 'retrieve' do - qs = person.question_sets.create - person.question_sets.retrieve(qs.id) - assert_requested(api_stub, times: 3) - end - - describe '#all' do - it 'requests' do - person.question_sets.all - assert_requested(api_stub, times: 2) - end - - it ':count' do - response = person.question_sets.all(count: 2) - expect(response.count).to eq(2) - assert_requested(api_stub, times: 2) - end - - it ':offset' do - response = person.question_sets.all(count: 2, offset: 2) - expect(response.count).to eq(2) - assert_requested(api_stub, times: 2) - end - end - - describe '#score' do - let(:answers) do - [ - { question_id: 1, answer_id: 1 }, - { question_id: 2, answer_id: 1 }, - { question_id: 3, answer_id: 1 }, - { question_id: 4, answer_id: 1 }, - { question_id: 5, answer_id: 1 } - ] + describe 'api requests' do + let(:api_stub) { @api_stub } + + context 'when creating' do + it 'create' do + person.question_sets.count + person.question_sets.create + assert_requested(api_stub, times: 2) + end + + it 'create question_set count' do + count = person.question_sets.count + person.question_sets.create + expect(count + 1).to be_truthy + assert_requested(api_stub, times: 2) + end end - it 'score call does request' do - qs = person.question_sets.create - qs.score(answers) - assert_requested(api_stub, times: 3) + context 'when scoring' do + let(:answers) do + [ + { question_id: 1, answer_id: 1 }, + { question_id: 2, answer_id: 1 }, + { question_id: 3, answer_id: 1 }, + { question_id: 4, answer_id: 1 }, + { question_id: 5, answer_id: 1 } + ] + end + + it 'score call does request' do + qs = person.question_sets.create + qs.score(answers) + assert_requested(api_stub, times: 3) + end end end end