diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea1eaac..b896a6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: if: matrix.ruby == '2.7' && github.event_name != 'pull_request' continue-on-error: ${{ matrix.allow_failures != 'false' }} - name: Run tests - run: bundle exec rake test + run: bundle exec rake - name: CodeClimate Post-build Notification run: cc-test-reporter after-build if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always() diff --git a/.gitignore b/.gitignore index c7d34be..16f2906 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /pkg/ /spec/reports/ /tmp/ +/rethinkdb_data *.gem diff --git a/.rubocop.yml b/.rubocop.yml index abcd1de..1425bf1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -32,3 +32,11 @@ Sequel/SaveChanges: Lint/UselessMethodDefinition: Enabled: false + +RSpec/FilePath: + Include: + - spec + - spec_orms + +RSpec/BeforeAfterAll: + Enabled: false \ No newline at end of file diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9eadb51..fff4955 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2021-03-21 02:34:29 UTC using RuboCop version 1.11.0. +# on 2021-03-24 02:21:52 UTC using RuboCop version 1.11.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -13,13 +13,6 @@ Gemspec/RequiredRubyVersion: Exclude: - 'omniauth-identity.gemspec' -# Offense count: 1 -# Configuration parameters: AllowedMethods. -# AllowedMethods: enums -Lint/ConstantDefinitionInBlock: - Exclude: - - 'spec/omniauth/identity/models/active_record_spec.rb' - # Offense count: 1 # Configuration parameters: AllowComments. Lint/EmptyClass: @@ -27,11 +20,11 @@ Lint/EmptyClass: - 'spec/omniauth/identity/secure_password_spec.rb' # Offense count: 1 -Lint/NestedMethodDefinition: +Lint/MissingSuper: Exclude: - 'lib/omniauth/identity/secure_password.rb' -# Offense count: 1 +# Offense count: 2 # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: Max: 30 @@ -40,14 +33,14 @@ Metrics/AbcSize: # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. # IgnoredMethods: refine Metrics/BlockLength: - Max: 28 + Max: 32 # Offense count: 1 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: Max: 145 -# Offense count: 7 +# Offense count: 9 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: Max: 20 @@ -82,44 +75,25 @@ Naming/PredicateName: - 'spec/**/*' - 'lib/omniauth/identity/secure_password.rb' -# Offense count: 7 +# Offense count: 6 # Configuration parameters: Prefixes. # Prefixes: when, with, without RSpec/ContextWording: Exclude: - - 'spec/omniauth/identity/model_spec.rb' - 'spec/omniauth/strategies/identity_spec.rb' + - 'spec/support/shared_contexts/persistable_model.rb' # Offense count: 1 # Configuration parameters: Max. RSpec/ExampleLength: Exclude: - - 'spec/omniauth/identity/model_spec.rb' + - 'spec/support/shared_contexts/instance_with_instance_methods.rb' # Offense count: 3 RSpec/ExpectInHook: Exclude: - 'spec/omniauth/strategies/identity_spec.rb' -# Offense count: 8 -# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. -# Include: **/*_spec*rb*, **/spec/**/* -RSpec/FilePath: - Exclude: - - 'spec/omniauth/identity/model_spec.rb' - - 'spec/omniauth/identity/models/active_record_spec.rb' - - 'spec/omniauth/identity/models/couch_potato_spec.rb' - - 'spec/omniauth/identity/models/mongoid_spec.rb' - - 'spec/omniauth/identity/models/no_brainer_spec.rb' - - 'spec/omniauth/identity/models/sequel_spec.rb' - - 'spec/omniauth/identity/secure_password_spec.rb' - - 'spec/omniauth/strategies/identity_spec.rb' - -# Offense count: 1 -RSpec/LeakyConstantDeclaration: - Exclude: - - 'spec/omniauth/identity/models/active_record_spec.rb' - # Offense count: 8 # Configuration parameters: . # SupportedStyles: have_received, receive @@ -135,13 +109,11 @@ RSpec/MultipleExpectations: RSpec/MultipleMemoizedHelpers: Max: 8 -# Offense count: 44 +# Offense count: 1 # Configuration parameters: IgnoreSharedExamples. RSpec/NamedSubject: Exclude: - - 'spec/omniauth/identity/model_spec.rb' - - 'spec/omniauth/identity/models/couch_potato_spec.rb' - - 'spec/omniauth/identity/models/mongoid_spec.rb' + - 'spec_orms/mongoid_spec.rb' # Offense count: 12 RSpec/NestedGroups: @@ -152,27 +124,28 @@ RSpec/StubbedMock: Exclude: - 'spec/omniauth/strategies/identity_spec.rb' -# Offense count: 22 +# Offense count: 5 RSpec/SubjectStub: Exclude: - - 'spec/omniauth/identity/model_spec.rb' - 'spec/omniauth/identity/models/active_record_spec.rb' - - 'spec/omniauth/identity/models/couch_potato_spec.rb' - - 'spec/omniauth/identity/models/mongoid_spec.rb' + - 'spec/omniauth/identity/models/sequel_spec.rb' + - 'spec_orms/couch_potato_spec.rb' + - 'spec_orms/mongoid_spec.rb' + - 'spec_orms/nobrainer_spec.rb' # Offense count: 4 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: - - 'spec/omniauth/identity/model_spec.rb' - 'spec/omniauth/strategies/identity_spec.rb' + - 'spec/support/shared_contexts/model_with_class_methods.rb' # Offense count: 1 Rake/Desc: Exclude: - 'Rakefile' -# Offense count: 7 +# Offense count: 10 Style/Documentation: Exclude: - '**/*.md' @@ -184,13 +157,19 @@ Style/Documentation: - 'lib/omniauth/identity/secure_password.rb' - 'spec/omniauth/identity/secure_password_spec.rb' -# Offense count: 5 +# Offense count: 4 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: - 'lib/omniauth/identity/model.rb' - 'lib/omniauth/identity/secure_password.rb' +# Offense count: 1 +# Cop supports --auto-correct. +Style/IfUnlessModifier: + Exclude: + - 'lib/omniauth/strategies/identity.rb' + # Offense count: 1 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? @@ -198,10 +177,10 @@ Style/OptionalBooleanParameter: Exclude: - 'lib/omniauth/identity/model.rb' -# Offense count: 1 +# Offense count: 4 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https # IgnoredPatterns: (?-mix:^\#) Layout/LineLength: - Max: 123 + Max: 138 diff --git a/.simplecov b/.simplecov index 8f9fa26..3e0a796 100644 --- a/.simplecov +++ b/.simplecov @@ -1,6 +1,10 @@ # frozen_string_literal: true SimpleCov.start do + add_filter '/.github/' + add_filter '/coverage/' + add_filter '/rethinkdb_data/' add_filter '/spec/' + add_filter '/spec_orms/' add_filter '/vendor/bundle/' end diff --git a/CHANGELOG.md b/CHANGELOG.md index c8be7a5..c682727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.0.7] - 2021-03-23 + +### Fixed + +- \[ActiveRecord\] Fixed [#110](https://github.com/omniauth/omniauth-identity/issues/110) which prevented `OmniAuth::Identity::Models::ActiveRecord`-based records from saving. +- \[CouchPotato\] Fixed `OmniAuth::Identity::Models::CouchPotato`'s `#save`. +- \[Sequel\] Fixed `OmniAuth::Identity::Models::Sequel`'s `#save`. +- \[Model\] Only define `::create`, `#save`, and `#persisted?` when not already defined. +- \[Model\] Restore original `info` functionality which set `name` based on `first_name`, `last_name`, or `nickname` + +### Changed + +- Upgraded to a newer `OmniAuth::Identity::SecurePassword` ripped from [Rails 6-1-stable](https://github.com/rails/rails/blob/6-1-stable/activemodel/lib/active_model/secure_password.rb) + - Aeons ago the original was ripped from Rails 3.1, and frozen in time. + While writing specs, it was discovered to be incompatible with this gem's Sequel adapter. + - Specs validate that the new version does work. + In any case, the ripped version is only used when the `has_secure_password` macro is not yet defined in the class. + +### Added + +- New specs to cover real use cases and implementations of each ORM model adapter that ships with the gem: + - ActiveRecord (Polyglot - Many Relational Databases) + - Sequel (Polyglot - Many Relational Databases) + - CouchPotato (CouchDB) + - Mongoid (MongoDB) + - NoBrainer (RethinkDB) + ## [3.0.6] - 2021-03-20 ### Fixed diff --git a/Gemfile b/Gemfile index c026a79..88ea1cb 100644 --- a/Gemfile +++ b/Gemfile @@ -17,11 +17,18 @@ group :documentation do end group :development, :test do + # ORMs if ruby_version < Gem::Version.new('2.5.0') - gem 'activerecord', '~> 5' # rails 5 works with Ruby 2.4 + gem 'activerecord', '~> 5', require: false # rails 5 works with Ruby 2.4 else - gem 'activerecord', '~> 6' # rails 6 requires Ruby 2.5 or later + gem 'activerecord', '~> 6', require: false # rails 6 requires Ruby 2.5 or later end + gem 'anonymous_active_record', '~> 1', require: false + gem 'couch_potato', github: 'langalex/couch_potato', require: false + gem 'mongoid', '~> 7', require: false + gem 'mongoid-rspec', github: 'mongoid/mongoid-rspec', require: false + gem 'nobrainer', '~> 0', require: false + gem 'sequel', '~> 5', require: false if ruby_version >= Gem::Version.new('2.4') # No need to run byebug / pry on earlier versions @@ -44,12 +51,10 @@ group :development, :test do gem 'simplecov', '~> 0.21', platform: :mri end - gem 'couch_potato', github: 'langalex/couch_potato' gem 'growl' gem 'guard' gem 'guard-bundler' gem 'guard-rspec' - gem 'mongoid-rspec', github: 'mongoid/mongoid-rspec' gem 'rb-fsevent' end diff --git a/README.md b/README.md index e6a238c..74200f8 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,8 @@ Just include `OmniAuth::Identity::Models::Sequel` mixin, and specify whatever else you will need. ```ruby -class SequelTestIdentity < Sequel::Model - include OmniAuth::Identity::Models::Sequel +class SequelTestIdentity < Sequel::Model(:identities) + include ::OmniAuth::Identity::Models::Sequel auth_key :email # whatever else you want! end @@ -108,8 +108,8 @@ fields that you will need. ```ruby class Identity - include Mongoid::Document - include OmniAuth::Identity::Models::Mongoid + include ::Mongoid::Document + include ::OmniAuth::Identity::Models::Mongoid field :email, type: String field :name, type: String @@ -124,8 +124,9 @@ fields that you will need. ```ruby class Identity - include CouchPotato::Persistence - include OmniAuth::Identity::Models::CouchPotatoModule + # NOTE: CouchPotato::Persistence must be included before OmniAuth::Identity::Models::CouchPotatoModule + include ::CouchPotato::Persistence + include ::OmniAuth::Identity::Models::CouchPotatoModule property :email property :password_digest @@ -147,8 +148,8 @@ fields that you will need. ```ruby class Identity - include NoBrainer::Document - include OmniAuth::Identity::Models::NoBrainer + include ::NoBrainer::Document + include ::OmniAuth::Identity::Models::NoBrainer auth_key :email end @@ -265,6 +266,49 @@ option :locate_conditions, ->(req) { { model.auth_key => req['auth_key'] } } Please contribute some documentation if you have the gumption! The maintainer's time is limited, and sometimes the authors of PRs with new options don't update the _this_ readme. 😭 +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am ‘Added some feature’`) +4. Push to the branch (`git push origin my-new-feature`) +5. Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally. + - NOTE: In order to run *all* the tests you will need to have the following databases installed, configured, and running. + 1. [RethinkDB](https://rethinkdb.com), an open source, real-time, web database, [installed](https://rethinkdb.com/docs/install/) and [running](https://rethinkdb.com/docs/start-a-server/), e.g. + ```bash + brew install rethinkdb + rethinkdb + ``` + 2. [MongoDB](https://docs.mongodb.com/manual/administration/install-community/) + ```bash + brew tap mongodb/brew + brew install mongodb-community@4.4 + mongod --config /usr/local/etc/mongod.conf + ``` + 3. [CouchDB](https://couchdb.apache.org) (download the .app) + To run all tests on all databases: + ```bash + bundle exec rake + ``` + To run a specific DB: + ```bash + # CouchDB / CouchPotato + bundle exec rspec spec spec_orms --tag 'couchdb' + + # ActiveRecord and Sequel, as they both use the in-memory SQLite driver. + bundle exec rspec spec spec_orms --tag 'sqlite3' + + # NOTE - mongoid and nobrainer specs can't be isolated with "tag" because it still loads everything, + # and the two libraries are fundamentally incompatible. + + # MongoDB / Mongoid + bundle exec rspec spec_orms/mongoid_spec.rb + + # RethinkDB / NoBrainer + bundle exec rspec spec_orms/nobrainer_spec.rb + ``` +6. Create new Pull Request + ## License MIT License. See LICENSE for details. diff --git a/Rakefile b/Rakefile index 362d638..a4cffdc 100644 --- a/Rakefile +++ b/Rakefile @@ -4,13 +4,26 @@ require 'bundler/gem_tasks' begin require 'rspec/core/rake_task' - RSpec::Core::RakeTask.new(:spec) + RSpec::Core::RakeTask.new(:test) + couch_potato = RSpec::Core::RakeTask.new(:spec_orm_couch_potato) + couch_potato.pattern = 'spec_orms/couch_potato_spec.rb' + mongoid = RSpec::Core::RakeTask.new(:spec_orm_mongoid) + mongoid.pattern = 'spec_orms/mongoid_spec.rb' + nobrainer = RSpec::Core::RakeTask.new(:spec_orm_nobrainer) + nobrainer.pattern = 'spec_orms/nobrainer_spec.rb' + + # When running all tests you must have RethinkDB, CouchDB, and MongoDB running. See README.md + task spec: %i[ + test + spec_orm_couch_potato + spec_orm_mongoid + spec_orm_nobrainer + ] rescue LoadError - task :spec do + task :test do warn 'RSpec is disabled' end end -task test: :spec begin require 'rubocop/rake_task' @@ -21,4 +34,5 @@ rescue LoadError end end -task default: [:test] +# These tests do not require any services to be running, so this is what we run via Github Actions +task default: %i[test] diff --git a/lib/omniauth/identity.rb b/lib/omniauth/identity.rb index 73ae8d9..7e25ba5 100644 --- a/lib/omniauth/identity.rb +++ b/lib/omniauth/identity.rb @@ -14,7 +14,7 @@ module Models autoload :ActiveRecord, 'omniauth/identity/models/active_record' autoload :Mongoid, 'omniauth/identity/models/mongoid' autoload :CouchPotatoModule, 'omniauth/identity/models/couch_potato' - autoload :NoBrainer, 'omniauth/identity/models/no_brainer' + autoload :NoBrainer, 'omniauth/identity/models/nobrainer' autoload :Sequel, 'omniauth/identity/models/sequel' end end diff --git a/lib/omniauth/identity/model.rb b/lib/omniauth/identity/model.rb index ec18166..26974be 100644 --- a/lib/omniauth/identity/model.rb +++ b/lib/omniauth/identity/model.rb @@ -25,11 +25,13 @@ module Model def self.included(base) base.extend ClassMethods + base.extend ClassCreateApi unless base.respond_to?(:create) + im = base.instance_methods + base.include InstanceSaveApi unless im.include?(:save) + base.include InstancePersistedApi unless im.include?(:persisted?) end module ClassMethods - extend Gem::Deprecate - # Authenticate a user with the given key and password. # # @param [String] key The unique login key provided for a given identity. @@ -52,52 +54,55 @@ def auth_key(method = false) @auth_key || 'email' end + # Locate an identity given its unique login key. + # + # @abstract + # @param [String] key The unique login key. + # @return [Model] An instance of the identity model class. + def locate(_key) + raise NotImplementedError + end + end + + module ClassCreateApi # Persists a new Identity object to the ORM. - # Defaults to calling super. Override as needed per ORM. + # Only included if the class doesn't define create, as a reminder to define create. + # Override as needed per ORM. # # @deprecated v4.0 will begin using {#new} with {#save} instead. # @abstract # @param [Hash] args Attributes of the new instance. # @return [Model] An instance of the identity model class. # @since 3.0.5 - def create(*args) - raise NotImplementedError unless defined?(super) - - super + def create(*_args) + raise NotImplementedError end + end - # Locate an identity given its unique login key. + module InstanceSaveApi + # Persists a new Identity object to the ORM. + # Default raises an error. Override as needed per ORM. + # This base version's arguments are modeled after ActiveModel + # since it is a pattern many ORMs follow # # @abstract - # @param [String] key The unique login key. # @return [Model] An instance of the identity model class. - def locate(key) + # @since 3.0.5 + def save(**_options, &_block) raise NotImplementedError end end - # Persists a new Identity object to the ORM. - # Default raises an error. Override as needed per ORM. - # - # @abstract - # @return [Model] An instance of the identity model class. - # @since 3.0.5 - def save - raise NotImplementedError unless defined?(super) - - super - end - - # Checks if the Identity object is persisted in the ORM. - # Defaults to calling super. Override as needed per ORM. - # - # @abstract - # @return [true or false] true if object exists, false if not. - # @since 3.0.5 - def persisted? - raise NotImplementedError unless defined?(super) - - super + module InstancePersistedApi + # Checks if the Identity object is persisted in the ORM. + # Default raises an error. Override as needed per ORM. + # + # @abstract + # @return [true or false] true if object exists, false if not. + # @since 3.0.5 + def persisted? + raise NotImplementedError + end end # Returns self if the provided password is correct, false @@ -106,7 +111,7 @@ def persisted? # @abstract # @param [String] password The password to check. # @return [self or false] Self if authenticated, false if not. - def authenticate(password) + def authenticate(_password) raise NotImplementedError end @@ -163,9 +168,13 @@ def auth_key=(value) # # @return [Hash] A string-keyed hash of user information. def info - SCHEMA_ATTRIBUTES.each_with_object({}) do |attribute, hash| + info = {} + SCHEMA_ATTRIBUTES.each_with_object(info) do |attribute, hash| hash[attribute] = send(attribute) if respond_to?(attribute) end + info['name'] ||= [info['first_name'], info['last_name']].join(' ').strip if info['first_name'] || info['last_name'] + info['name'] ||= info['nickname'] + info end end end diff --git a/lib/omniauth/identity/models/active_record.rb b/lib/omniauth/identity/models/active_record.rb index af0f3f0..4012633 100644 --- a/lib/omniauth/identity/models/active_record.rb +++ b/lib/omniauth/identity/models/active_record.rb @@ -6,8 +6,8 @@ module OmniAuth module Identity module Models class ActiveRecord < ::ActiveRecord::Base - include OmniAuth::Identity::Model - include OmniAuth::Identity::SecurePassword + include ::OmniAuth::Identity::Model + include ::OmniAuth::Identity::SecurePassword self.abstract_class = true has_secure_password diff --git a/lib/omniauth/identity/models/couch_potato.rb b/lib/omniauth/identity/models/couch_potato.rb index 9431095..463ec1f 100644 --- a/lib/omniauth/identity/models/couch_potato.rb +++ b/lib/omniauth/identity/models/couch_potato.rb @@ -7,6 +7,7 @@ module Identity module Models # can not be named CouchPotato since there is a class with that name # NOTE: CouchPotato is based on ActiveModel. + # NOTE: CouchPotato::Persistence must be included before OmniAuth::Identity::Models::CouchPotatoModule module CouchPotatoModule def self.included(base) base.class_eval do @@ -23,6 +24,10 @@ def self.auth_key=(key) def self.locate(search_hash) where(search_hash).first end + + def save + CouchPotato.database.save(self) + end end end end diff --git a/lib/omniauth/identity/models/no_brainer.rb b/lib/omniauth/identity/models/nobrainer.rb similarity index 100% rename from lib/omniauth/identity/models/no_brainer.rb rename to lib/omniauth/identity/models/nobrainer.rb diff --git a/lib/omniauth/identity/models/sequel.rb b/lib/omniauth/identity/models/sequel.rb index 7f65881..41fa054 100644 --- a/lib/omniauth/identity/models/sequel.rb +++ b/lib/omniauth/identity/models/sequel.rb @@ -6,9 +6,9 @@ module OmniAuth module Identity module Models # http://sequel.jeremyevans.net/ an SQL ORM - # NOTE: Sequel is *not* based on ActiveModel, but supports the API we need, except for `persisted`: + # NOTE: Sequel is *not* based on ActiveModel, but supports the API we need, except for `persisted?`: # * create - # * save, but save is deprecated in favor of `save_changes` + # * save module Sequel def self.included(base) base.class_eval do @@ -17,7 +17,7 @@ def self.included(base) # plugin :validation_helpers plugin :validation_class_methods - include OmniAuth::Identity::Model + include ::OmniAuth::Identity::Model include ::OmniAuth::Identity::SecurePassword has_secure_password @@ -38,7 +38,7 @@ def persisted? end def save - save_changes + super end end end diff --git a/lib/omniauth/identity/secure_password.rb b/lib/omniauth/identity/secure_password.rb index 826d4b9..ee8fa57 100644 --- a/lib/omniauth/identity/secure_password.rb +++ b/lib/omniauth/identity/secure_password.rb @@ -4,7 +4,7 @@ module OmniAuth module Identity - # This is taken directly from Rails 3.1 code and is used if + # This is lightly edited from Rails 6.1 code and is used if # the version of ActiveModel that's being used does not # include SecurePassword. The only difference is that instead of # using ActiveSupport::Concern, it checks to see if there is already @@ -14,63 +14,124 @@ def self.included(base) base.extend ClassMethods unless base.respond_to?(:has_secure_password) end + # BCrypt hash function can handle maximum 72 bytes, and if we pass + # password of length more than 72 bytes it ignores extra characters. + # Hence need to put a restriction on password length. + MAX_PASSWORD_LENGTH_ALLOWED = 72 + + class << self + attr_accessor :min_cost # :nodoc: + end + self.min_cost = false + module ClassMethods # Adds methods to set and authenticate against a BCrypt password. - # This mechanism requires you to have a password_digest attribute. + # This mechanism requires you to have a +XXX_digest+ attribute. + # Where +XXX+ is the attribute name of your desired password. + # + # The following validations are added automatically: + # * Password must be present on creation + # * Password length should be less than or equal to 72 bytes + # * Confirmation of password (using a +XXX_confirmation+ attribute) # - # Validations for presence of password, confirmation of password (using - # a "password_confirmation" attribute) are automatically added. - # You can add more validations by hand if need be. + # If confirmation validation is not needed, simply leave out the + # value for +XXX_confirmation+ (i.e. don't provide a form field for + # it). When this attribute has a +nil+ value, the validation will not be + # triggered. + # + # For further customizability, it is possible to suppress the default + # validations by passing validations: false as an argument. + # + # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password: + # + # gem 'bcrypt', '~> 3.1.7' # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # - # # Schema: User(name:string, password_digest:string) + # # Schema: User(name:string, password_digest:string, recovery_password_digest:string) # class User < ActiveRecord::Base # has_secure_password + # has_secure_password :recovery_password, validations: false # end # - # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch") - # user.save # => false, password required - # user.password = "mUc3m00RsqyRe" - # user.save # => false, confirmation doesn't match - # user.password_confirmation = "mUc3m00RsqyRe" - # user.save # => true - # user.authenticate("notright") # => false - # user.authenticate("mUc3m00RsqyRe") # => user - # User.find_by_name("david").try(:authenticate, "notright") # => nil - # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user - def has_secure_password - attr_reader :password + # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch') + # user.save # => false, password required + # user.password = 'mUc3m00RsqyRe' + # user.save # => false, confirmation doesn't match + # user.password_confirmation = 'mUc3m00RsqyRe' + # user.save # => true + # user.recovery_password = "42password" + # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC" + # user.save # => true + # user.authenticate('notright') # => false + # user.authenticate('mUc3m00RsqyRe') # => user + # user.authenticate_recovery_password('42password') # => user + # User.find_by(name: 'david')&.authenticate('notright') # => false + # User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user + def has_secure_password(attribute = :password, validations: true) + # Load bcrypt gem only when has_secure_password is used. + # This is to avoid ActiveModel (and by extension the entire framework) + # being dependent on a binary library. + begin + require 'bcrypt' + rescue LoadError + warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install" + raise + end - validates_confirmation_of :password - validates_presence_of :password_digest + include InstanceMethodsOnActivation.new(attribute) - include InstanceMethodsOnActivation + if validations + include ActiveModel::Validations - if respond_to?(:attributes_protected_by_default) - def self.attributes_protected_by_default - super + ['password_digest'] + # This ensures the model has a password by checking whether the password_digest + # is present, so that this works with both new and existing records. However, + # when there is an error, the message is added to the password attribute instead + # so that the error message will make sense to the end-user. + validate do |record| + record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present? end + + validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED + validates_confirmation_of attribute, allow_blank: true end end end - module InstanceMethodsOnActivation - # Returns self if the password is correct, otherwise false. - def authenticate(unencrypted_password) - if BCrypt::Password.new(password_digest) == unencrypted_password - self - else - false + class InstanceMethodsOnActivation < Module + def initialize(attribute) + attr_reader attribute + + define_method("#{attribute}=") do |unencrypted_password| + if unencrypted_password.nil? + public_send("#{attribute}_digest=", nil) + elsif !unencrypted_password.empty? + instance_variable_set("@#{attribute}", unencrypted_password) + cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost + public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost)) + end end - end - # Encrypts the password into the password_digest attribute. - def password=(unencrypted_password) - @password = unencrypted_password - if unencrypted_password && !unencrypted_password.empty? - self.password_digest = BCrypt::Password.create(unencrypted_password) + define_method("#{attribute}_confirmation=") do |unencrypted_password| + instance_variable_set("@#{attribute}_confirmation", unencrypted_password) end + + # Returns +self+ if the password is correct, otherwise +false+. + # + # class User < ActiveRecord::Base + # has_secure_password validations: false + # end + # + # user = User.new(name: 'david', password: 'mUc3m00RsqyRe') + # user.save + # user.authenticate_password('notright') # => false + # user.authenticate_password('mUc3m00RsqyRe') # => user + define_method("authenticate_#{attribute}") do |unencrypted_password| + attribute_digest = public_send("#{attribute}_digest") + BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self + end + + alias_method :authenticate, :authenticate_password if attribute == :password end end end diff --git a/omniauth-identity.gemspec b/omniauth-identity.gemspec index e407e29..0cb2fa1 100644 --- a/omniauth-identity.gemspec +++ b/omniauth-identity.gemspec @@ -8,13 +8,10 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'bcrypt' gem.add_runtime_dependency 'omniauth' - gem.add_development_dependency 'anonymous_active_record', '~> 1.0', '>= 1.0.8' - gem.add_development_dependency 'mongoid', '~> 7' - gem.add_development_dependency 'nobrainer', '~> 0' gem.add_development_dependency 'rack-test', '~> 1' gem.add_development_dependency 'rake', '~> 13' gem.add_development_dependency 'rspec', '~> 3' - gem.add_development_dependency 'sequel', '~> 5' + gem.add_development_dependency 'rspec-block_is_expected', '~> 1.0' gem.add_development_dependency 'sqlite3', '~> 1.4' # NOTE: Released version of couch_potato depends on activemodel ~> 4.0, so pull latest from github in Gemfile. # gem.add_development_dependency 'couch_potato', '~> 1.7' diff --git a/spec/omniauth/identity/model_spec.rb b/spec/omniauth/identity/model_spec.rb index c2ce2ee..5a8a40c 100644 --- a/spec/omniauth/identity/model_spec.rb +++ b/spec/omniauth/identity/model_spec.rb @@ -1,123 +1,43 @@ # frozen_string_literal: true -class ExampleModel - include OmniAuth::Identity::Model -end - RSpec.describe OmniAuth::Identity::Model do - context 'Class Methods' do - subject { ExampleModel } - - describe '.locate' do - it('is abstract') { expect { subject.locate('abc') }.to raise_error(NotImplementedError) } + before do + identity_test_klass = Class.new do + include OmniAuth::Identity::Model end + stub_const('IdentityTestClass', identity_test_klass) + end - describe '.authenticate' do - it 'calls locate and then authenticate' do - mocked_instance = double('ExampleModel', authenticate: 'abbadoo') - allow(subject).to receive(:locate).with('email' => 'example').and_return(mocked_instance) - expect(subject.authenticate({ 'email' => 'example' }, 'pass')).to eq('abbadoo') - end + describe 'Class Methods' do + subject(:model_klass) { IdentityTestClass } - it 'calls locate with additional scopes when provided' do - mocked_instance = double('ExampleModel', authenticate: 'abbadoo') - allow(subject).to receive(:locate).with('email' => 'example', - 'user_type' => 'admin').and_return(mocked_instance) - expect(subject.authenticate({ 'email' => 'example', 'user_type' => 'admin' }, 'pass')).to eq('abbadoo') - end + include_context 'model with class methods' - it 'recovers gracefully if locate is nil' do - allow(subject).to receive(:locate).and_return(nil) - expect(subject.authenticate('blah', 'foo')).to be false + describe '::locate' do + it('is abstract') do + expect { model_klass.locate('email' => 'example') }.to raise_error(NotImplementedError) end end end - context 'Instance Methods' do - subject { ExampleModel.new } + describe 'Instance Methods' do + subject(:instance) { IdentityTestClass.new } - describe '#authenticate' do - it('is abstract') { expect { subject.authenticate('abc') }.to raise_error(NotImplementedError) } - end + include_context 'instance with instance methods' - describe '#uid' do - it 'defaults to #id' do - allow(subject).to receive(:respond_to?).with(:id).and_return(true) - allow(subject).to receive(:id).and_return 'wakka-do' - expect(subject.uid).to eq('wakka-do') - end - - it 'stringifies it' do - allow(subject).to receive(:id).and_return 123 - expect(subject.uid).to eq('123') - end - - it 'raises NotImplementedError if #id is not defined' do - allow(subject).to receive(:respond_to?).with(:id).and_return(false) - expect { subject.uid }.to raise_error(NotImplementedError) - end + describe '#authenticate' do + it('is abstract') { expect { instance.authenticate('my-password') }.to raise_error(NotImplementedError) } end describe '#auth_key' do - it 'defaults to #email' do - allow(subject).to receive(:respond_to?).with(:email).and_return(true) - allow(subject).to receive(:email).and_return('bob@bob.com') - expect(subject.auth_key).to eq('bob@bob.com') - end - - it 'uses the class .auth_key' do - subject.class.auth_key 'login' - allow(subject).to receive(:login).and_return 'bob' - expect(subject.auth_key).to eq('bob') - subject.class.auth_key nil - end - it 'raises a NotImplementedError if the auth_key method is not defined' do - expect { subject.auth_key }.to raise_error(NotImplementedError) + expect { instance.auth_key }.to raise_error(NotImplementedError) end end describe '#auth_key=' do - it 'defaults to setting email' do - allow(subject).to receive(:respond_to?).with(:email=).and_return(true) - expect(subject).to receive(:email=).with 'abc' - - subject.auth_key = 'abc' - end - - it 'uses a custom .auth_key if one is provided' do - subject.class.auth_key 'login' - allow(subject).to receive(:respond_to?).with(:login=).and_return(true) - expect(subject).to receive(:login=).with('abc') - - subject.auth_key = 'abc' - end - - it 'raises a NotImplementedError if the autH_key method is not defined' do - expect { subject.auth_key = 'broken' }.to raise_error(NotImplementedError) - end - end - - describe '#info' do - it 'includes attributes that are set' do - allow(subject).to receive(:name).and_return('Bob Bobson') - allow(subject).to receive(:nickname).and_return('bob') - - expect(subject.info).to eq({ - 'name' => 'Bob Bobson', - 'nickname' => 'bob' - }) - end - - it 'automaticallies set name off of nickname' do - allow(subject).to receive(:nickname).and_return('bob') - subject.info['name'] == 'bob' - end - - it 'does not overwrite a provided name' do - allow(subject).to receive(:name).and_return('Awesome Dude') - allow(subject).to receive(:first_name).and_return('Frank') - expect(subject.info['name']).to eq('Awesome Dude') + it 'raises a NotImplementedError if the auth_key method is not defined' do + expect { instance.auth_key = 'broken' }.to raise_error(NotImplementedError) end end end diff --git a/spec/omniauth/identity/models/active_record_spec.rb b/spec/omniauth/identity/models/active_record_spec.rb index e4d9bce..c69c9d5 100644 --- a/spec/omniauth/identity/models/active_record_spec.rb +++ b/spec/omniauth/identity/models/active_record_spec.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true -RSpec.describe(OmniAuth::Identity::Models::ActiveRecord, db: true) do +require 'sqlite3' +require 'active_record' +require 'anonymous_active_record' + +class TestIdentity < OmniAuth::Identity::Models::ActiveRecord; end + +RSpec.describe(OmniAuth::Identity::Models::ActiveRecord, sqlite3: true) do describe 'model', type: :model do subject(:model_klass) do AnonymousActiveRecord.generate( @@ -14,17 +20,20 @@ def flower end end - it 'delegates locate to the where query method' do - allow(model_klass).to receive(:where).with('ham_sandwich' => 'open faced', 'category' => 'sandwiches', - 'provider' => 'identity').and_return(['wakka']) - expect(model_klass.locate('ham_sandwich' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') + include_context 'persistable model' + + describe '::table_name' do + it 'does not use STI rules for its table name' do + expect(TestIdentity.table_name).to eq('test_identities') + end end - end - describe '#table_name' do - class TestIdentity < OmniAuth::Identity::Models::ActiveRecord; end - it 'does not use STI rules for its table name' do - expect(TestIdentity.table_name).to eq('test_identities') + describe '::locate' do + it 'delegates locate to the where query method' do + allow(model_klass).to receive(:where).with('email' => 'open faced', 'category' => 'sandwiches', + 'provider' => 'identity').and_return(['wakka']) + expect(model_klass.locate('email' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') + end end end end diff --git a/spec/omniauth/identity/models/couch_potato_spec.rb b/spec/omniauth/identity/models/couch_potato_spec.rb deleted file mode 100644 index 8022c4d..0000000 --- a/spec/omniauth/identity/models/couch_potato_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'couch_potato' - -class CouchPotatoTestIdentity - include CouchPotato::Persistence - include OmniAuth::Identity::Models::CouchPotatoModule - auth_key :ham_sandwich -end - -RSpec.describe(OmniAuth::Identity::Models::CouchPotatoModule, db: true) do - describe 'model', type: :model do - subject { CouchPotatoTestIdentity } - - it 'delegates locate to the where query method' do - allow(subject).to receive(:where).with('ham_sandwich' => 'open faced', - 'category' => 'sandwiches').and_return(['wakka']) - expect(subject.locate('ham_sandwich' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') - end - end -end diff --git a/spec/omniauth/identity/models/mongoid_spec.rb b/spec/omniauth/identity/models/mongoid_spec.rb deleted file mode 100644 index c3a91da..0000000 --- a/spec/omniauth/identity/models/mongoid_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'mongoid' - -class MongoidTestIdentity - include Mongoid::Document - include OmniAuth::Identity::Models::Mongoid - auth_key :ham_sandwich - store_in database: 'db1', collection: 'mongoid_test_identities', client: 'secondary' -end - -RSpec.describe(OmniAuth::Identity::Models::Mongoid, db: true) do - describe 'model', type: :model do - subject { MongoidTestIdentity } - - it { is_expected.to be_mongoid_document } - - it 'does not munge collection name' do - expect(subject).to be_stored_in(database: 'db1', collection: 'mongoid_test_identities', client: 'secondary') - end - - it 'delegates locate to the where query method' do - allow(subject).to receive(:where).with('ham_sandwich' => 'open faced', - 'category' => 'sandwiches').and_return(['wakka']) - expect(subject.locate('ham_sandwich' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') - end - end -end diff --git a/spec/omniauth/identity/models/no_brainer_spec.rb b/spec/omniauth/identity/models/no_brainer_spec.rb deleted file mode 100644 index 661be43..0000000 --- a/spec/omniauth/identity/models/no_brainer_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'nobrainer' - -class NobrainerTestIdentity - include NoBrainer::Document - include OmniAuth::Identity::Models::NoBrainer - auth_key :ham_sandwich -end - -RSpec.describe(OmniAuth::Identity::Models::NoBrainer, db: true) do - it 'delegates locate to the where query method' do - allow(NobrainerTestIdentity).to receive(:where).with('ham_sandwich' => 'open faced', - 'category' => 'sandwiches').and_return(['wakka']) - expect(NobrainerTestIdentity.locate('ham_sandwich' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') - end -end diff --git a/spec/omniauth/identity/models/sequel_spec.rb b/spec/omniauth/identity/models/sequel_spec.rb index 8805482..3198b29 100644 --- a/spec/omniauth/identity/models/sequel_spec.rb +++ b/spec/omniauth/identity/models/sequel_spec.rb @@ -1,23 +1,38 @@ # frozen_string_literal: true +require 'sqlite3' require 'sequel' -# Connect to an in-memory sqlite3 database. + DB = Sequel.sqlite -DB.create_table :sequel_test_identities do - primary_key :id - String :ham_sandwich, null: false - String :password_digest, null: false -end -class SequelTestIdentity < Sequel::Model - include OmniAuth::Identity::Models::Sequel - auth_key :ham_sandwich -end +RSpec.describe(OmniAuth::Identity::Models::Sequel, sqlite3: true) do + before(:all) do + # Connect to an in-memory sqlite3 database. + DB.create_table :sequel_test_identities do + primary_key :id + String :email, null: false + String :password_digest, null: false + end + end + + before do + sequel_test_identity = Class.new(Sequel::Model(:sequel_test_identities)) do + include ::OmniAuth::Identity::Models::Sequel + end + stub_const('SequelTestIdentity', sequel_test_identity) + end + + describe 'model', type: :model do + subject(:model_klass) { SequelTestIdentity } + + include_context 'persistable model' -RSpec.describe(OmniAuth::Identity::Models::Sequel, db: true) do - it 'delegates locate to the where query method' do - allow(SequelTestIdentity).to receive(:where).with('ham_sandwich' => 'open faced', - 'category' => 'sandwiches').and_return(['wakka']) - expect(SequelTestIdentity.locate('ham_sandwich' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') + describe '::locate' do + it 'delegates to the where query method' do + allow(model_klass).to receive(:where).with('email' => 'open faced', + 'category' => 'sandwiches').and_return(['wakka']) + expect(model_klass.locate('email' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') + end + end end end diff --git a/spec/omniauth/strategies/identity_spec.rb b/spec/omniauth/strategies/identity_spec.rb index a05e5d6..0417c66 100644 --- a/spec/omniauth/strategies/identity_spec.rb +++ b/spec/omniauth/strategies/identity_spec.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true -RSpec.describe OmniAuth::Strategies::Identity do +require 'sqlite3' +require 'active_record' +require 'anonymous_active_record' + +RSpec.describe OmniAuth::Strategies::Identity, sqlite3: true do attr_accessor :app let(:env_hash) { last_response.headers['env'] } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 832eb54..ae75d70 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,21 +1,32 @@ # frozen_string_literal: true +# NOTE: mongoid and no_brainer can't be loaded at the same time. +# If you try it, one or both of them will not work. +# This is why the ORM specs are split into a separate directory and run in separate threads. + +ENV['RUBY_ENV'] = 'test' # Used by NoBrainer +ENV['MONGOID_ENV'] = 'test' # Used by Mongoid + ruby_version = Gem::Version.new(RUBY_VERSION) require 'simplecov' if ruby_version >= Gem::Version.new('2.7') && RUBY_ENGINE == 'ruby' require 'rack/test' -require 'mongoid-rspec' -require 'sqlite3' -require 'sequel' -require 'anonymous_active_record' +require 'rspec/block_is_expected' require 'byebug' if RUBY_ENGINE == 'ruby' # This gem require 'omniauth/identity' +spec_root_matcher = %r{#{__dir__}/(.+)\.rb\Z} +Dir.glob(Pathname.new(__dir__).join('support/**/', '*.rb')).each { |f| require f.match(spec_root_matcher)[1] } + +DEFAULT_PASSWORD = 'hang-a-left-at-the-diner' +DEFAULT_EMAIL = 'mojo@example.com' + RSpec.configure do |config| config.include Rack::Test::Methods - config.include Mongoid::Matchers, type: :model + + # config.include ::Mongoid::Matchers, db: :mongodb # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = '.rspec_status' diff --git a/spec/support/shared_contexts/instance_with_instance_methods.rb b/spec/support/shared_contexts/instance_with_instance_methods.rb new file mode 100644 index 0000000..edac011 --- /dev/null +++ b/spec/support/shared_contexts/instance_with_instance_methods.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'instance with instance methods' do + describe '#initialize' do + it 'does not raise an error' do + block_is_expected.not_to raise_error + end + end + + describe '#uid' do + it 'defaults to #id' do + allow(instance).to receive(:respond_to?).with(:id).and_return(true) + allow(instance).to receive(:id).and_return 'wakka-do' + expect(instance.uid).to eq('wakka-do') + end + + it 'stringifies it' do + allow(instance).to receive(:id).and_return 123 + expect(instance.uid).to eq('123') + end + + it 'raises NotImplementedError if #id is not defined' do + allow(instance).to receive(:respond_to?).with(:id).and_return(false) + expect { instance.uid }.to raise_error(NotImplementedError) + end + end + + describe '#auth_key' do + it 'defaults to #email' do + allow(instance).to receive(:respond_to?).with(:email).and_return(true) + allow(instance).to receive(:email).and_return('bob@bob.com') + expect(instance.auth_key).to eq('bob@bob.com') + end + + it 'uses the class .auth_key' do + instance.class.auth_key 'login' + allow(instance).to receive(:login).and_return 'bob' + expect(instance.auth_key).to eq('bob') + instance.class.auth_key nil + end + end + + describe '#auth_key=' do + it 'defaults to setting email' do + allow(instance).to receive(:respond_to?).with(:email=).and_return(true) + expect(instance).to receive(:email=).with 'abc' + + instance.auth_key = 'abc' + end + + it 'uses a custom .auth_key if one is provided' do + instance.class.auth_key 'login' + allow(instance).to receive(:respond_to?).with(:login=).and_return(true) + expect(instance).to receive(:login=).with('abc') + + instance.auth_key = 'abc' + end + end + + describe '#info' do + it 'includes all attributes as they have been set' do + allow(instance).to receive(:name).and_return('Bob Bobson') + allow(instance).to receive(:nickname).and_return('bob') + + expect(instance.info).to include({ + 'name' => 'Bob Bobson', + 'nickname' => 'bob' + }) + end + + it 'uses firstname and lastname, over nickname, to set missing name' do + allow(instance).to receive(:first_name).and_return('shoeless') + allow(instance).to receive(:last_name).and_return('joe') + allow(instance).to receive(:nickname).and_return('george') + instance.info['name'] == 'shoeless joe' + end + + it 'uses nickname to set missing name when first and last are not set' do + allow(instance).to receive(:nickname).and_return('bob') + instance.info['name'] == 'bob' + end + + it 'does not overwrite a provided name' do + allow(instance).to receive(:name).and_return('Awesome Dude') + allow(instance).to receive(:first_name).and_return('Frank') + expect(instance.info['name']).to eq('Awesome Dude') + end + end +end diff --git a/spec/support/shared_contexts/model_with_class_methods.rb b/spec/support/shared_contexts/model_with_class_methods.rb new file mode 100644 index 0000000..8295283 --- /dev/null +++ b/spec/support/shared_contexts/model_with_class_methods.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'model with class methods' do + describe 'class definition' do + it 'does not raise an error' do + block_is_expected.not_to raise_error + end + end + + describe '::authenticate' do + it 'calls locate and then authenticate' do + mocked_instance = double('ExampleModel', authenticate: 'abbadoo') + allow(model_klass).to receive(:locate).with('email' => 'example').and_return(mocked_instance) + expect(model_klass.authenticate({ 'email' => 'example' }, 'pass')).to eq('abbadoo') + end + + it 'calls locate with additional scopes when provided' do + mocked_instance = double('ExampleModel', authenticate: 'abbadoo') + allow(model_klass).to receive(:locate).with('email' => 'example', + 'user_type' => 'admin').and_return(mocked_instance) + expect(model_klass.authenticate({ 'email' => 'example', 'user_type' => 'admin' }, 'pass')).to eq('abbadoo') + end + + it 'recovers gracefully if locate is nil' do + allow(model_klass).to receive(:locate).and_return(nil) + expect(model_klass.authenticate('blah', 'foo')).to be false + end + end +end diff --git a/spec/support/shared_contexts/persistable_model.rb b/spec/support/shared_contexts/persistable_model.rb new file mode 100644 index 0000000..d0d6319 --- /dev/null +++ b/spec/support/shared_contexts/persistable_model.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.shared_context 'persistable model' do + include_context 'model with class methods' + + describe 'instance methods' do + subject(:instance) { model_klass.new } + + include_context 'instance with instance methods' + + describe '#save' do + subject(:save) do + instance.email = DEFAULT_EMAIL + instance.password = DEFAULT_PASSWORD + instance.password_confirmation = DEFAULT_PASSWORD + instance.save + end + + it 'does not raise an error' do + save + end + end + end +end diff --git a/spec_orms/couch_potato_spec.rb b/spec_orms/couch_potato_spec.rb new file mode 100644 index 0000000..fc8fc34 --- /dev/null +++ b/spec_orms/couch_potato_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'couch_potato' + +RSpec.describe(OmniAuth::Identity::Models::CouchPotatoModule, couchdb: true) do + before(:all) do + CouchPotato::Config.database_name = 'http://admin:butterknuckles@127.0.0.1:5984/test' + end + + before do + couch_potato_test_identity = Class.new do + # NOTE: CouchPotato::Persistence must be included before OmniAuth::Identity::Models::CouchPotatoModule + include ::CouchPotato::Persistence + include ::OmniAuth::Identity::Models::CouchPotatoModule + property :email + property :password_digest + end + stub_const('CouchPotatoTestIdentity', couch_potato_test_identity) + end + + describe 'model', type: :model do + subject(:model_klass) { CouchPotatoTestIdentity } + + include_context 'persistable model' + + describe '::locate' do + it 'delegates to the where query method' do + allow(model_klass).to receive(:where).with('email' => 'open faced', + 'category' => 'sandwiches').and_return(['wakka']) + expect(model_klass.locate('email' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') + end + end + end +end diff --git a/spec_orms/mongoid_spec.rb b/spec_orms/mongoid_spec.rb new file mode 100644 index 0000000..29f9d3c --- /dev/null +++ b/spec_orms/mongoid_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# NOTE: mongoid and no_brainer can't be loaded at the same time. +# If you try it, one or both of them will not work. +require_relative 'support/rspec_config/mongoid' + +RSpec.describe(OmniAuth::Identity::Models::Mongoid, mongodb: true) do + describe 'model', type: :model do + subject(:model_klass) { MongoidTestIdentity } + + it { is_expected.to be_mongoid_document } + + it 'does not munge collection name' do + expect(subject).to be_stored_in(database: 'db1', collection: 'mongoid_test_identities', client: 'default') + end + + include_context 'persistable model' + + describe '::locate' do + it 'delegates to the where query method' do + allow(model_klass).to receive(:where).with('email' => 'open faced', + 'category' => 'sandwiches').and_return(['wakka']) + expect(model_klass.locate('email' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') + end + end + end +end diff --git a/spec_orms/nobrainer_spec.rb b/spec_orms/nobrainer_spec.rb new file mode 100644 index 0000000..e34d7cf --- /dev/null +++ b/spec_orms/nobrainer_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# NOTE: mongoid and nobrainer can't be loaded at the same time. +# If you try it, one or both of them will not work. +require 'nobrainer' + +RSpec.describe(OmniAuth::Identity::Models::NoBrainer, rethinkdb: true) do + before(:all) do + NoBrainer.configure do |config| + config.app_name = 'DeezBrains' + config.rethinkdb_urls = ['rethinkdb://127.0.0.1:28015/DeezBrains_test'] + config.table_options = { shards: 1, replicas: 1, + write_acks: :majority } + end + NoBrainer.sync_schema + end + + before do + nobrainer_test_identity = Class.new do + include ::NoBrainer::Document + include ::OmniAuth::Identity::Models::NoBrainer + field :email + field :password_digest + end + stub_const('NoBrainerTestIdentity', nobrainer_test_identity) + NoBrainer.purge! + end + + describe 'model', type: :model do + subject(:model_klass) { NoBrainerTestIdentity } + + include_context 'persistable model' + + describe '::locate' do + it 'delegates locate to the where query method' do + allow(model_klass).to receive(:where).with('email' => 'open faced', + 'category' => 'sandwiches').and_return(['wakka']) + expect(model_klass.locate('email' => 'open faced', 'category' => 'sandwiches')).to eq('wakka') + end + end + end +end diff --git a/spec_orms/support/rspec_config/mongoid.rb b/spec_orms/support/rspec_config/mongoid.rb new file mode 100644 index 0000000..b65436c --- /dev/null +++ b/spec_orms/support/rspec_config/mongoid.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'mongoid' +require 'mongoid-rspec' + +RSpec.configure do |config| + config.include ::Mongoid::Matchers, mongodb: true +end + +Mongoid.load!(File.join(__dir__, 'mongoid.yml')) + +class MongoidTestIdentity + include ::Mongoid::Document + include ::OmniAuth::Identity::Models::Mongoid + store_in database: 'db1', collection: 'mongoid_test_identities' + field :email, type: String + field :password_digest, type: String +end diff --git a/spec_orms/support/rspec_config/mongoid.yml b/spec_orms/support/rspec_config/mongoid.yml new file mode 100644 index 0000000..711c978 --- /dev/null +++ b/spec_orms/support/rspec_config/mongoid.yml @@ -0,0 +1,8 @@ +test: + clients: + default: + database: mongoid_test + hosts: + - localhost:27017 + options: + server_selection_timeout: 1