diff --git a/CHANGELOG.md b/CHANGELOG.md index 19814976..c1db20ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## HEAD * Fix ruby 2.7 deprecation warnings [#241](https://github.com/Sorcery/sorcery/pull/241) +* Fix Thor-related warning in generator [#252](https://github.com/Sorcery/sorcery/pull/252) +* Check submodule availability in generator [#252](https://github.com/Sorcery/sorcery/pull/252) ## 0.15.0 diff --git a/Gemfile b/Gemfile index 532fc3ce..fa75df15 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,3 @@ source 'https://rubygems.org' -gem 'pry' -gem 'rails' -gem 'rails-controller-testing' -gem 'sqlite3' - gemspec diff --git a/Rakefile b/Rakefile index 2470119d..6877f832 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,21 @@ require 'bundler/gem_tasks' - require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) task default: :spec + +task :console do + require 'byebug' + require 'pry' + require 'rails' + require 'sorcery' + + def reload! + files = $LOADED_FEATURES.select { |feat| feat =~ /\/sorcery\// } + files.each { |file| load file } + end + + ARGV.clear + Pry.start +end diff --git a/lib/generators/sorcery/helpers.rb b/lib/generators/sorcery/helpers.rb index 5bc94408..5ef1c03f 100644 --- a/lib/generators/sorcery/helpers.rb +++ b/lib/generators/sorcery/helpers.rb @@ -3,8 +3,28 @@ module Generators module Helpers private - def sorcery_config_path - 'config/initializers/sorcery.rb' + def initializer_path + File.join(initializers_path, 'sorcery.rb') + end + + def initializers_path + File.join('config', 'initializers') + end + + def migration_class_name + if Rails::VERSION::MAJOR >= 5 + "ActiveRecord::Migration[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" + else + 'ActiveRecord::Migration' + end + end + + def migration_path(submodule) + File.join(migrations_path, "sorcery_#{submodule}.rb") + end + + def migrations_path + File.join('db', 'migrate') end # Either return the model passed in a classified form or return the default "User". @@ -12,32 +32,51 @@ def model_class_name options[:model] ? options[:model].classify : 'User' end - def tableized_model_class - options[:model] ? options[:model].gsub(/::/, '').tableize : 'User' + def model_injection + indents = model_class_name.split('::').count + indents += 1 if namespace + + "#{' ' * indents}authenticates_with_sorcery!\n" + end + + def model_injection_point + "class #{model_class_name} < #{model_superclass_name}\n" + end + + def model_name + if namespace + [namespace.to_s] + [model_class_name] + else + [model_class_name] + end.join('::') end def model_path - @model_path ||= File.join('app', 'models', "#{file_path}.rb") + File.join(models_path, "#{model_name.underscore}.rb") + end + + def models_path + File.join('app', 'models') end - def file_path - model_name.underscore + def model_superclass_name + if Rails::VERSION::MAJOR >= 5 + 'ApplicationRecord' + else + 'ActiveRecord::Base' + end end def namespace Rails::Generators.namespace if Rails::Generators.respond_to?(:namespace) end - def namespaced? - !!namespace + def only_submodules? + options[:migrations] || options[:only_submodules] end - def model_name - if namespaced? - [namespace.to_s] + [model_class_name] - else - [model_class_name] - end.join('::') + def tableized_model_class + model_class_name.gsub(/::/, '').tableize end end end diff --git a/lib/generators/sorcery/install_generator.rb b/lib/generators/sorcery/install_generator.rb index c3fefa32..00186096 100644 --- a/lib/generators/sorcery/install_generator.rb +++ b/lib/generators/sorcery/install_generator.rb @@ -7,6 +7,16 @@ class InstallGenerator < Rails::Generators::Base include Rails::Generators::Migration include Sorcery::Generators::Helpers + AVAILABLE_SUBMODULES = %w[ + activity_logging + brute_force_protection + external + magic_login + remember_me + reset_password + user_activation + ].freeze + source_root File.expand_path('templates', __dir__) argument :submodules, optional: true, type: :array, banner: 'submodules' @@ -26,49 +36,52 @@ def check_deprecated_options warn('[DEPRECATED] `--migrations` option is deprecated, please use `--only-submodules` instead') end - # Copy the initializer file to config/initializers folder. - def copy_initializer_file - template 'initializer.rb', sorcery_config_path unless only_submodules? + def check_available_submodules + return unless submodules + + guidance = "Try some of these: #{AVAILABLE_SUBMODULES.join(', ')}" + + submodules.each do |submodule| + raise ArgumentError, "#{submodule} is not a Sorcery submodule. #{guidance}" unless + AVAILABLE_SUBMODULES.include?(submodule) + end end - def configure_initializer_file - # Add submodules to the initializer file. + # Copy the initializer file to config/initializers folder, and add the submodules if necessary. + def install_initializer + template 'initializer.rb', initializer_path unless only_submodules? + return unless submodules submodule_names = submodules.collect { |submodule| ':' + submodule } - gsub_file sorcery_config_path, /submodules = \[.*\]/ do |str| + gsub_file initializer_path, /submodules = \[.*\]/ do |str| current_submodule_names = (str =~ /\[(.*)\]/ ? Regexp.last_match(1) : '').delete(' ').split(',') "submodules = [#{(current_submodule_names | submodule_names).join(', ')}]" end end - def configure_model + def install_model # Generate the model and add 'authenticates_with_sorcery!' unless you passed --only-submodules return if only_submodules? generate "model #{model_class_name} --skip-migration" - inject_sorcery_to_model - end - - def inject_sorcery_to_model - indents = ' ' * (namespaced? ? 2 : 1) - - inject_into_class(model_path, model_class_name, "#{indents}authenticates_with_sorcery!\n") + + inject_into_class(model_path, model_class_name, model_injection, after: model_injection_point) end # Copy the migrations files to db/migrate folder - def copy_migration_files + def install_migrations # Copy core migration file in all cases except when you pass --only-submodules. return unless defined?(ActiveRecord) - migration_template 'migration/core.rb', 'db/migrate/sorcery_core.rb', migration_class_name: migration_class_name unless only_submodules? + migration_template 'migration/core.rb', migration_path(:core), migration_class_name: migration_class_name unless only_submodules? return unless submodules submodules.each do |submodule| unless %w[http_basic_auth session_timeout core].include?(submodule) - migration_template "migration/#{submodule}.rb", "db/migrate/sorcery_#{submodule}.rb", migration_class_name: migration_class_name + migration_template "migration/#{submodule}.rb", migration_path(submodule), migration_class_name: migration_class_name end end end @@ -82,20 +95,6 @@ def self.next_migration_number(dirname) format('%.3d', (current_migration_number(dirname) + 1)) end end - - private - - def only_submodules? - options[:migrations] || options[:only_submodules] - end - - def migration_class_name - if Rails::VERSION::MAJOR >= 5 - "ActiveRecord::Migration[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" - else - 'ActiveRecord::Migration' - end - end end end end diff --git a/sorcery.gemspec b/sorcery.gemspec index 7a90e92b..7b492b1a 100644 --- a/sorcery.gemspec +++ b/sorcery.gemspec @@ -38,9 +38,13 @@ Gem::Specification.new do |s| s.add_dependency 'oauth2', '~> 1.0', '>= 0.8.0' s.add_development_dependency 'byebug', '~> 10.0.0' + s.add_development_dependency 'pry' + s.add_development_dependency 'rails' + s.add_development_dependency 'rails-controller-testing' s.add_development_dependency 'rspec-rails', '~> 3.7.0' s.add_development_dependency 'rubocop' s.add_development_dependency 'simplecov', '>= 0.3.8' + s.add_development_dependency 'sqlite3' s.add_development_dependency 'test-unit', '~> 3.2.0' s.add_development_dependency 'timecop' s.add_development_dependency 'webmock', '~> 3.3.0' diff --git a/spec/generators/install_generator_spec.rb b/spec/generators/install_generator_spec.rb new file mode 100644 index 00000000..eb0730be --- /dev/null +++ b/spec/generators/install_generator_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' +require 'support/generator_helper' + +require 'generators/sorcery/install_generator' + +describe 'install generator' do + include GeneratorHelper + + class TestInstallGenerator < Sorcery::Generators::InstallGenerator + source_root File.expand_path('../../lib/generators/sorcery/templates', __dir__) + + def self.next_migration_number(dirname) + current_migration_number(dirname) + 1 + end + end + + tests TestInstallGenerator + + context 'given deprecated --migrations option' do + let(:installer) do + generator(options: { migrations: true }) + end + + it 'shows warning' do + expect(installer).to receive(:warn).with(/\[DEPRECATED\] `--migrations` option is deprecated/) + installer.check_deprecated_options + end + end + + context 'given invalid submodule' do + it 'raises error' do + expect { + generator('invalid_submodule').check_available_submodules + }.to raise_error(ArgumentError).with_message(/invalid_submodule is not a Sorcery submodule/) + end + end + + describe 'initializer' do + describe 'installation' do + let(:installation) { invoke!(:install_initializer) } + + it 'creates initializer' do + expect(installation).to match(/create config\/initializers\/sorcery.rb/) + end + end + + describe 'configuration' do + context 'given submodule(s)' do + let(:initializer_contents) do + File.read(initializer_path) + end + + before do + invoke!(:install_initializer, 'activity_logging') + end + + it 'adds submodule(s) to initializer' do + expect(initializer_contents).to match(/Rails\.application\.config\.sorcery\.submodules = \[:activity_logging\]/) + end + end + end + + describe 'uninstallation' do + let(:uninstallation) { revoke!(:install_initializer) } + + before do + invoke!(:install_initializer) + end + + it 'removes initializer' do + expect(uninstallation).to match(/remove config\/initializers\/sorcery.rb/) + end + end + end + + describe 'model' do + describe 'installation' do + let(:installation) { invoke!(:install_model) } + + it 'skips migration' do + expect(installation).to match(/generate model User \-\-skip\-migration/) + end + + it 'creates model' do + expect(installation).to match(/create app\/models\/user\.rb/) + end + end + + describe 'configuration' do + let(:model_contents) do + File.read(model_path(:user)) + end + + before do + invoke!(:install_model) + end + + it 'adds `authenticates_with_sorcery!`' do + expect(model_contents).to match(/authenticates_with_sorcery!/) + end + end + + describe 'uninstallation' do + let(:uninstallation) { revoke!(:install_model) } + + before do + invoke!(:install_model) + end + + it 'removes `authenticates_with_sorcery!`' do + expect(uninstallation).to match(/subtract app\/models\/user.rb/) + end + end + end + + describe 'migrations' do + describe 'installation' do + let(:installation) { invoke!(:install_migrations, 'activity_logging', options: { only_submodules: true }) } + + it 'creates migration' do + expect(installation).to match(/create db\/migrate\/1_sorcery_activity_logging.rb/) + end + end + + describe 'uninstallation' do + let(:uninstallation) { revoke!(:install_migrations, 'activity_logging', options: { only_submodules: true }) } + + it 'removes migration' do + expect(uninstallation).to match(/remove db\/migrate\/1_sorcery_activity_logging.rb/) + end + end + end +end diff --git a/spec/rails_app/bin/rails b/spec/rails_app/bin/rails new file mode 100644 index 00000000..cf808618 --- /dev/null +++ b/spec/rails_app/bin/rails @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby + +# Really, this should come from spec_helper.rb somehow -- but for some reason, doesn't work +# when running generators... this is why it's added here. Hopefully someone a little wiser +# than I can fix this. +# +SORCERY_ORM = :active_record + +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bc5c48aa..cab42a12 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,6 +9,7 @@ # SimpleCov.root File.join(File.dirname(__FILE__), '..', 'lib') # SimpleCov.start require 'rails/all' +require 'rails-controller-testing' require 'rspec/rails' require 'timecop' require 'byebug' diff --git a/spec/support/generator_helper.rb b/spec/support/generator_helper.rb new file mode 100644 index 00000000..6172b410 --- /dev/null +++ b/spec/support/generator_helper.rb @@ -0,0 +1,91 @@ +require 'active_support/concern' +require 'active_support/testing/stream' +require 'rails/generators' + +module GeneratorHelper + extend ActiveSupport::Concern + include ActiveSupport::Testing::Stream + include FileUtils + + included do |base| + class_attribute :destination_root, default: File.expand_path('../rails_app', __dir__) + class_attribute :generator_class + + base.teardown :remove_installation! + end + + module ClassMethods + def tests(klass) + self.generator_class = klass + end + + def destination(path) + self.destination_root = path + end + end + + # Instantiate the generator. + def generator(*args, options: {}, config: {}) + generator_class.new(args, options, config.reverse_merge(destination_root: destination_root)) + end + + # Invoke a specific action + def invoke!(action, *args, options: {}, config: {}) + gen = generator(*args, options: options, config: config.reverse_merge(behavior: :invoke)) + + capture(:stdout) do + gen.invoke(action) + end + end + + # Revoke a specific action + def revoke!(action, *args, options: {}, config: {}) + gen = generator(*args, options: options, config: config.reverse_merge(behavior: :revoke)) + + capture(:stdout) do + gen.invoke(action) + end + end + + def initializer_path + @initializer_path ||= File.join(destination_root, 'config', 'initializers', 'sorcery.rb') + end + + def migrations_path + @migrations_path ||= File.join(destination_root, 'db', 'migrate') + end + + # def migration_path(migration) + # @migration_path ||= {} + # @migration_path[migration.to_s] ||= Dir.glob("#{migrations_path}/[0-9]*_*.rb") + # .grep(/\d+_#{migration}.rb$/) + # .first + # end + + def model_path(model) + @model_path ||= {} + @model_path[model.to_s] ||= File.join(destination_root, 'app', 'models', "#{model}.rb") + end + +private + + def remove_installation! + # Remove any generated initializers, models, migrations files + files = [initializer_path] + files += Dir.glob(File.join(destination_root, 'app', 'models', '*.rb')) + files += Dir.glob(File.join(destination_root, 'db', 'migrate', '*.rb')) + + files.each do |file| + rm_f(file) if File.exists?(file) + end + + # Recursively remove full directories + dirs = [ + File.join(destination_root, 'test') + ] + + dirs.each do |dir| + rm_rf(dir) if Dir.exists?(dir) + end + end +end \ No newline at end of file