diff options
Diffstat (limited to 'railties')
56 files changed, 999 insertions, 115 deletions
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 7aee513a99..3afadc8cba 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Fix running multiple tests in one `rake` command e.g. `bin/rake test:models test:controllers` @@ -68,7 +70,7 @@ *DHH* -* Add Yarn support in new apps with a yarn binstub and vendor/package.json. Skippable via --skip-yarn option. +* Add Yarn support in new apps with a yarn binstub and package.json. Skippable via --skip-yarn option. *Liceth Ovalles*, *Guillermo Iguaran*, *DHH* diff --git a/railties/lib/rails/api/generator.rb b/railties/lib/rails/api/generator.rb new file mode 100644 index 0000000000..dcc491783c --- /dev/null +++ b/railties/lib/rails/api/generator.rb @@ -0,0 +1,28 @@ +require "sdoc" + +class RDoc::Generator::API < RDoc::Generator::SDoc # :nodoc: + RDoc::RDoc.add_generator self + + def generate_class_tree_level(classes, visited = {}) + # Only process core extensions on the first visit. + if visited.empty? + core_exts, classes = classes.partition { |klass| core_extension?(klass) } + + super.unshift([ "Core extensions", "", "", build_core_ext_subtree(core_exts, visited) ]) + else + super + end + end + + private + def build_core_ext_subtree(classes, visited) + classes.map do |klass| + [ klass.name, klass.document_self_or_methods ? klass.path : "", "", + generate_class_tree_level(klass.classes_and_modules, visited) ] + end + end + + def core_extension?(klass) + klass.name != "ActiveSupport" && klass.in_files.any? { |file| file.absolute_name.include?("core_ext") } + end +end diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index bc670b1d75..49267c2329 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -1,4 +1,5 @@ require "rdoc/task" +require_relative "generator" module Rails module API @@ -8,8 +9,7 @@ module Rails include: %w( README.rdoc lib/active_support/**/*.rb - ), - exclude: "lib/active_support/vendor/*" + ) }, "activerecord" => { @@ -69,7 +69,11 @@ module Rails README.rdoc lib/**/*.rb ), - exclude: "lib/rails/generators/rails/**/templates/**/*.rb" + exclude: %w( + lib/rails/generators/**/templates/**/*.rb + lib/rails/test_unit/* + lib/rails/api/generator.rb + ) } } @@ -80,7 +84,7 @@ module Rails # Be lazy computing stuff to have as light impact as possible to # the rest of tasks. before_running_rdoc do - load_and_configure_sdoc + configure_sdoc configure_rdoc_files setup_horo_variables end @@ -91,20 +95,15 @@ module Rails # no-op end - def load_and_configure_sdoc - require "sdoc" - + def configure_sdoc self.title = "Ruby on Rails API" self.rdoc_dir = api_dir options << "-m" << api_main options << "-e" << "UTF-8" - options << "-f" << "sdoc" + options << "-f" << "api" options << "-T" << "rails" - rescue LoadError - $stderr.puts %(Unable to load SDoc, please add\n\n gem 'sdoc', require: false\n\nto the Gemfile.) - exit 1 end def configure_rdoc_files @@ -147,7 +146,7 @@ module Rails end class RepoTask < Task - def load_and_configure_sdoc + def configure_sdoc super options << "-g" # link to GitHub, SDoc flag end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 1a6aed7ce4..89f7b5991f 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -4,6 +4,7 @@ require "active_support/core_ext/object/blank" require "active_support/key_generator" require "active_support/message_verifier" require "rails/engine" +require "rails/secrets" module Rails # An Engine with the responsibility of coordinating the whole boot process. @@ -385,18 +386,7 @@ module Rails def secrets @secrets ||= begin secrets = ActiveSupport::OrderedOptions.new - yaml = config.paths["config/secrets"].first - - if File.exist?(yaml) - require "erb" - - all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {} - shared_secrets = all_secrets["shared"] - env_secrets = all_secrets[Rails.env] - - secrets.merge!(shared_secrets.deep_symbolize_keys) if shared_secrets - secrets.merge!(env_secrets.deep_symbolize_keys) if env_secrets - end + secrets.merge! Rails::Secrets.parse(config.paths["config/secrets"].existent, env: Rails.env) # Fallback to config.secret_key_base if secrets.secret_key_base isn't set secrets.secret_key_base ||= config.secret_key_base diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 6102af3fff..4223c38146 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -2,6 +2,7 @@ require "fileutils" require "active_support/notifications" require "active_support/dependencies" require "active_support/descendants_tracker" +require "rails/secrets" module Rails class Application @@ -77,6 +78,11 @@ INFO initializer :bootstrap_hook, group: :all do |app| ActiveSupport.run_load_hooks(:before_initialize, app) end + + initializer :set_secrets_root, group: :all do + Rails::Secrets.root = root + Rails::Secrets.read_encrypted_secrets = config.read_encrypted_secrets + end end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index b0d33f87a3..b0592151b7 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -13,7 +13,8 @@ module Rails :railties_order, :relative_url_root, :secret_key_base, :secret_token, :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, - :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading + :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, + :read_encrypted_secrets attr_writer :log_level attr_reader :encoding, :api_only @@ -51,6 +52,7 @@ module Rails @debug_exception_response_format = nil @x = Custom.new @enable_dependency_loading = false + @read_encrypted_secrets = false end def encoding=(value) @@ -80,7 +82,7 @@ module Rails @paths ||= begin paths = super paths.add "config/database", with: "config/database.yml" - paths.add "config/secrets", with: "config/secrets.yml" + paths.add "config/secrets", with: "config", glob: "secrets.yml{,.enc}" paths.add "config/environment", with: "config/environment.rb" paths.add "lib/templates" paths.add "log", with: "log/#{Rails.env}.log" diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb index 13f3b90b6d..0d4e6dc5a1 100644 --- a/railties/lib/rails/command.rb +++ b/railties/lib/rails/command.rb @@ -27,15 +27,23 @@ module Rails end # Receives a namespace, arguments and the behavior to invoke the command. - def invoke(namespace, args = [], **config) - namespace = namespace.to_s - namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) - namespace = "version" if %w( -v --version ).include? namespace + def invoke(full_namespace, args = [], **config) + namespace = full_namespace = full_namespace.to_s - if command = find_by_namespace(namespace) - command.perform(namespace, args, config) + if char = namespace =~ /:(\w+)$/ + command_name, namespace = $1, namespace.slice(0, char) else - find_by_namespace("rake").perform(namespace, args, config) + command_name = namespace + end + + command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name) + command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name) + + command = find_by_namespace(namespace, command_name) + if command && command.all_commands[command_name] + command.perform(command_name, args, config) + else + find_by_namespace("rake").perform(full_namespace, args, config) end end @@ -52,8 +60,10 @@ module Rails # # Notice that "rails:commands:webrat" could be loaded as well, what # Rails looks for is the first and last parts of the namespace. - def find_by_namespace(name) # :nodoc: - lookups = [ name, "rails:#{name}" ] + def find_by_namespace(namespace, command_name = nil) # :nodoc: + lookups = [ namespace ] + lookups << "#{namespace}:#{command_name}" if command_name + lookups.concat lookups.map { |lookup| "rails:#{lookup}" } lookup(lookups) diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb index fb80e9d997..8fda1c87c6 100644 --- a/railties/lib/rails/command/actions.rb +++ b/railties/lib/rails/command/actions.rb @@ -8,16 +8,16 @@ module Rails Dir.chdir(File.expand_path("../../", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) end - if defined?(ENGINE_PATH) - def require_application_and_environment! - require ENGINE_PATH + def require_application_and_environment! + require ENGINE_PATH if defined?(ENGINE_PATH) - if defined?(APP_PATH) - require APP_PATH - Rails.application.require_environment! - end + if defined?(APP_PATH) + require APP_PATH + Rails.application.require_environment! end + end + if defined?(ENGINE_PATH) def load_tasks Rake.application.init("rails") Rake.application.load_rakefile @@ -29,11 +29,6 @@ module Rails engine.load_generators end else - def require_application_and_environment! - require APP_PATH - Rails.application.require_environment! - end - def load_tasks Rails.application.load_tasks end diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb index 1435792536..db20c71861 100644 --- a/railties/lib/rails/command/base.rb +++ b/railties/lib/rails/command/base.rb @@ -56,7 +56,9 @@ module Rails end def perform(command, args, config) # :nodoc: - command = nil if Rails::Command::HELP_MAPPINGS.include?(args.first) + if Rails::Command::HELP_MAPPINGS.include?(args.first) + command, args = "help", [] + end dispatch(command, args.dup, nil, config) end @@ -111,7 +113,7 @@ module Rails # For a `Rails::Command::TestCommand` placed in `rails/command/test_command.rb` # would return `rails/test`. def default_command_root - path = File.expand_path(File.join("../commands", command_name), __dir__) + path = File.expand_path(File.join("../commands", command_root_namespace), __dir__) path if File.exist?(path) end @@ -129,6 +131,10 @@ module Rails super end end + + def command_root_namespace + (namespace.split(":") - %w( rails )).first + end end def help diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE new file mode 100644 index 0000000000..4b7deb4e2a --- /dev/null +++ b/railties/lib/rails/commands/secrets/USAGE @@ -0,0 +1,52 @@ +=== Storing Encrypted Secrets in Source Control + +The Rails `secrets` commands helps encrypting secrets to slim a production +environment's `ENV` hash. It's also useful for atomic deploys: no need to +coordinate key changes to get everything working as the keys are shipped +with the code. + +=== Setup + +Run `bin/rails secrets:setup` to opt in and generate the `config/secrets.yml.key` +and `config/secrets.yml.enc` files. + +The latter contains all the keys to be encrypted while the former holds the +encryption key. + +Don't lose the key! Put it in a password manager your team can access. +Should you lose it no one, including you, will be able to access any encrypted +secrets. +Don't commit the key! Add `config/secrets.yml.key` to your source control's +ignore file. If you use Git, Rails handles this for you. + +Rails also looks for the key in `ENV["RAILS_MASTER_KEY"]` if that's easier to +manage. + +You could prepend that to your server's start command like this: + + RAILS_MASTER_KEY="im-the-master-now-hahaha" server.start + + +The `config/secrets.yml.enc` has much the same format as `config/secrets.yml`: + + production: + secret_key_base: so-secret-very-hidden-wow + payment_processing_gateway_key: much-safe-very-gaedwey-wow + +But that's where the similarities between `secrets.yml` and `secrets.yml.enc` +end, e.g. no keys from `secrets.yml` will be moved to `secrets.yml.enc` and +be encrypted. + +A `shared:` top level key is also supported such that any keys there is merged +into the other environments. + +=== Editing Secrets + +After `bin/rails secrets:setup`, run `bin/rails secrets:edit`. + +That command opens a temporary file in `$EDITOR` with the decrypted contents of +`config/secrets.yml.enc` to edit the encrypted secrets. + +When the temporary file is next saved the contents are encrypted and written to +`config/secrets.yml.enc` while the file itself is destroyed to prevent secrets +from leaking. diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb new file mode 100644 index 0000000000..3ba8c0c85b --- /dev/null +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -0,0 +1,36 @@ +require "active_support" +require "rails/secrets" + +module Rails + module Command + class SecretsCommand < Rails::Command::Base # :nodoc: + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + + def setup + require "rails/generators" + require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" + + Rails::Generators::EncryptedSecretsGenerator.start + end + + def edit + require_application_and_environment! + + Rails::Secrets.read_for_editing do |tmp_path| + puts "Waiting for secrets file to be saved. Abort with Ctrl-C." + system("\$EDITOR #{tmp_path}") + end + + puts "New secrets encrypted and saved." + rescue Interrupt + puts "Aborted changing encrypted secrets: nothing saved." + rescue Rails::Secrets::MissingKeyError => error + say error.message + end + end + end +end diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb index 7bf8f61137..629fb5b425 100644 --- a/railties/lib/rails/commands/test/test_command.rb +++ b/railties/lib/rails/commands/test/test_command.rb @@ -11,7 +11,7 @@ module Rails def perform(*) $LOAD_PATH << Rails::Command.root.join("test") - Minitest.run_via[:rails] = true + Minitest.run_via = :rails require "active_support/testing/autorun" end diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 9c49e0655a..3174ffb0dc 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -8,7 +8,7 @@ module Rails MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 99bda728ee..3f1bf6a5bb 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -62,6 +62,7 @@ module Rails stylesheets: true, stylesheet_engine: :css, scaffold_stylesheet: true, + system_tests: nil, test_framework: false, template_engine: :erb } @@ -151,6 +152,7 @@ module Rails "#{test}:controller", "#{test}:helper", "#{test}:integration", + "#{test}:system", "#{test}:mailer", "#{test}:model", "#{test}:scaffold", @@ -212,6 +214,7 @@ module Rails rails.map! { |n| n.sub(/^rails:/, "") } rails.delete("app") rails.delete("plugin") + rails.delete("encrypted_secrets") hidden_namespaces.each { |n| groups.delete(n.to_s) } diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index ea88afe9f4..04f6341471 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -82,6 +82,9 @@ module Rails class_option :skip_test, type: :boolean, aliases: "-T", default: false, desc: "Skip test files" + class_option :skip_system_test, type: :boolean, default: false, + desc: "Skip system test files" + class_option :dev, type: :boolean, default: false, desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" @@ -190,7 +193,7 @@ module Rails def webserver_gemfile_entry # :doc: return [] if options[:skip_puma] comment = "Use Puma as the app server" - GemfileEntry.new("puma", "~> 3.0", comment) + GemfileEntry.new("puma", "~> 3.7", comment) end def include_all_railties? # :doc: @@ -243,7 +246,6 @@ module Rails def rails_gemfile_entry dev_edge_common = [ - GemfileEntry.github("arel", "rails/arel") ] if options.dev? [ @@ -261,14 +263,13 @@ module Rails end def rails_version_specifier(gem_version = Rails.gem_version) - if gem_version.prerelease? - next_series = gem_version - next_series = next_series.bump while next_series.segments.size > 2 - - [">= #{gem_version}", "< #{next_series}"] - elsif gem_version.segments.size == 3 + if gem_version.segments.size == 3 || gem_version.release.segments.size == 3 + # ~> 1.2.3 + # ~> 1.2.3.pre4 "~> #{gem_version}" else + # ~> 1.2.3, >= 1.2.3.4 + # ~> 1.2.3, >= 1.2.3.4.pre5 patch = gem_version.segments[0, 3].join(".") ["~> #{patch}", ">= #{gem_version}"] end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 3cf923faf0..86326d98ed 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -150,6 +150,12 @@ module Rails template "test/test_helper.rb" end + def system_test + empty_directory_with_keep_file "test/system" + + template "test/application_system_test_case.rb" + end + def tmp empty_directory_with_keep_file "tmp" empty_directory "tmp/cache" @@ -160,7 +166,7 @@ module Rails empty_directory_with_keep_file "vendor" unless options[:skip_yarn] - template "package.json", "vendor/package.json" + template "package.json" end end end @@ -262,6 +268,10 @@ module Rails build(:test) unless options[:skip_test] end + def create_system_test_files + build(:system_test) unless options[:skip_system_test] || options[:skip_test] || options[:api] + end + def create_tmp_files build(:tmp) end @@ -270,7 +280,7 @@ module Rails build(:vendor) if options[:skip_yarn] - remove_file "vendor/package.json" + remove_file "package.json" end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 24d2fa1284..b082d70cba 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -32,6 +32,11 @@ end group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + <%- unless options.skip_system_test? || options.api? -%> + # Adds support for Capybara system testing and selenium driver + gem 'capybara', '~> 2.7.0' + gem 'selenium-webdriver' + <%- end -%> end group :development do diff --git a/railties/lib/rails/generators/rails/app/templates/bin/yarn b/railties/lib/rails/generators/rails/app/templates/bin/yarn index 872438cecb..4ae896a8d3 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/yarn +++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn @@ -1,4 +1,4 @@ -VENDOR_PATH = File.expand_path('../vendor', __dir__) +VENDOR_PATH = File.expand_path('..', __dir__) Dir.chdir(VENDOR_PATH) do begin exec "yarnpkg #{ARGV.join(" ")}" diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 4a39e43e57..9c4a77fd1d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -14,6 +14,11 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true + # Attempt to read encrypted secrets from `config/secrets.yml.enc`. + # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or + # `config/secrets.yml.key`. + config.read_encrypted_secrets = true + # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt index f5d03fb117..51196ae743 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt @@ -7,7 +7,7 @@ Rails.application.config.assets.version = '1.0' # Rails.application.config.assets.paths << Emoji.images_path <%- unless options[:skip_yarn] -%> # Add Yarn node_modules folder to the asset load path. -Rails.application.config.assets.paths << Rails.root.join('vendor/node_modules') +Rails.application.config.assets.paths << Rails.root.join('node_modules') <%- end -%> # Precompile additional assets. diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml index 8e995a5df1..816efcc5b1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml @@ -23,8 +23,10 @@ development: test: secret_key_base: <%= app_secret %> -# Do not keep production secrets in the repository, -# instead read values from the environment. +# Do not keep production secrets in the unencrypted secrets file. +# Instead, either read values from the environment. +# Or, use `bin/rails secrets:setup` to configure encrypted secrets +# and move the `production:` environment over there. production: secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore index 1768b700d9..7221c26729 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -22,8 +22,8 @@ <% end -%> <% unless options[:skip_yarn] -%> -/vendor/node_modules -/vendor/yarn-error.log +/node_modules +/yarn-error.log <% end -%> .byebug_history diff --git a/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb new file mode 100644 index 0000000000..8b29213610 --- /dev/null +++ b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb @@ -0,0 +1,66 @@ +require "rails/generators/base" +require "rails/secrets" + +module Rails + module Generators + class EncryptedSecretsGenerator < Base + def add_secrets_key_file + unless File.exist?("config/secrets.yml.key") || File.exist?("config/secrets.yml.enc") + key = Rails::Secrets.generate_key + + say "Adding config/secrets.yml.key to store the encryption key: #{key}" + say "" + say "Save this in a password manager your team can access." + say "" + say "If you lose the key, no one, including you, can access any encrypted secrets." + + say "" + create_file "config/secrets.yml.key", key + say "" + end + end + + def ignore_key_file + if File.exist?(".gitignore") + unless File.read(".gitignore").include?(key_ignore) + say "Ignoring config/secrets.yml.key so it won't end up in Git history:" + say "" + append_to_file ".gitignore", key_ignore + say "" + end + else + say "IMPORTANT: Don't commit config/secrets.yml.key. Add this to your ignore file:" + say key_ignore, :on_green + say "" + end + end + + def add_encrypted_secrets_file + unless File.exist?("config/secrets.yml.enc") + say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted." + say "" + + template "config/secrets.yml.enc" do |prefill| + say "" + say "For now the file contains this but it's been encrypted with the generated key:" + say "" + say prefill, :on_green + say "" + + Secrets.encrypt(prefill) + end + + say "You can edit encrypted secrets with `bin/rails secrets:edit`." + + say "Add this to your config/environments/production.rb:" + say "config.read_encrypted_secrets = true" + end + end + + private + def key_ignore + [ "", "# Ignore encrypted secrets key file.", "config/secrets.yml.key", "" ].join("\n") + end + end + end +end diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc new file mode 100644 index 0000000000..70426a66a5 --- /dev/null +++ b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc @@ -0,0 +1,3 @@ +# See `secrets.yml` for tips on generating suitable keys. +# production: +# external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289… diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index d8a41d4afe..3fcd8607f0 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -91,6 +91,7 @@ task default: :test opts[:skip_bundle] = true opts[:api] = options.api? opts[:skip_listen] = true + opts[:skip_git] = true invoke Rails::Generators::AppGenerator, [ File.expand_path(dummy_path, destination_root) ], opts @@ -112,7 +113,6 @@ task default: :test def test_dummy_clean inside dummy_path do - remove_file ".gitignore" remove_file "db/seeds.rb" remove_file "doc" remove_file "Gemfile" diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt index c0fbb84a93..8385e6a8a2 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt @@ -1,10 +1,4 @@ -$: << File.expand_path(File.expand_path('../../test', __FILE__)) +$: << File.expand_path(File.expand_path("../../test", __FILE__)) -require 'bundler/setup' -require 'rails/test_unit/minitest_plugin' - -Rails::TestUnitReporter.executable = 'bin/test' - -Minitest.run_via[:rails] = true - -require "active_support/testing/autorun" +require "bundler/setup" +require "rails/plugin/test" diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb index ed6bf7f7d7..12d6bc85b2 100644 --- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -6,6 +6,7 @@ module Rails remove_hook_for :resource_controller remove_class_option :actions + class_option :api, type: :boolean class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets" class_option :stylesheet_engine, desc: "Engine for Stylesheets" class_option :assets, type: :boolean @@ -15,10 +16,13 @@ module Rails def handle_skip @options = @options.merge(stylesheets: false) unless options[:assets] @options = @options.merge(stylesheet_engine: false) unless options[:stylesheets] && options[:scaffold_stylesheet] + @options = @options.merge(system_tests: false) if options[:api] end hook_for :scaffold_controller, required: true + hook_for :system_tests, as: :system + hook_for :assets do |assets| invoke assets, [controller_name] end diff --git a/railties/lib/rails/generators/rails/system_test/USAGE b/railties/lib/rails/generators/rails/system_test/USAGE new file mode 100644 index 0000000000..f11a99e008 --- /dev/null +++ b/railties/lib/rails/generators/rails/system_test/USAGE @@ -0,0 +1,10 @@ +Description: + Stubs out a new system test. Pass the name of the test, either + CamelCased or under_scored, as an argument. + + This generator invokes the current system tool, which defaults to + TestUnit. + +Example: + `rails generate system_test GeneralStories` creates a GeneralStories + system test in test/system/general_stories_test.rb diff --git a/railties/lib/rails/generators/rails/system_test/system_test_generator.rb b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb new file mode 100644 index 0000000000..901120e892 --- /dev/null +++ b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb @@ -0,0 +1,7 @@ +module Rails + module Generators + class SystemTestGenerator < NamedBase # :nodoc: + hook_for :system_tests, as: :system + end + end +end diff --git a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb index dea7e22196..118e0f1271 100644 --- a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb +++ b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb @@ -1,7 +1,9 @@ require 'test_helper' +<% module_namespacing do -%> class <%= class_name %>Test < ActionDispatch::IntegrationTest # test "the truth" do # assert true # end end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/system/system_generator.rb b/railties/lib/rails/generators/test_unit/system/system_generator.rb new file mode 100644 index 0000000000..aec415a4e5 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb @@ -0,0 +1,17 @@ +require "rails/generators/test_unit" + +module TestUnit # :nodoc: + module Generators # :nodoc: + class SystemGenerator < Base # :nodoc: + check_class_collision suffix: "Test" + + def create_test_files + if !File.exist?(File.join("test/application_system_test_case.rb")) + template "application_system_test_case.rb", File.join("test", "application_system_test_case.rb") + end + + template "system_test.rb", File.join("test/system", "#{file_name.pluralize}_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/test_unit/system/templates/system_test.rb b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb new file mode 100644 index 0000000000..b5ce2ba5c8 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb @@ -0,0 +1,9 @@ +require "application_system_test_case" + +class <%= class_name.pluralize %>Test < ApplicationSystemTestCase + # test "visiting the index" do + # visit <%= plural_table_name %>_url + # + # assert_selector "h1", text: "<%= class_name %>" + # end +end diff --git a/railties/lib/rails/plugin/test.rb b/railties/lib/rails/plugin/test.rb new file mode 100644 index 0000000000..ff043b488e --- /dev/null +++ b/railties/lib/rails/plugin/test.rb @@ -0,0 +1,7 @@ +require "rails/test_unit/minitest_plugin" + +Rails::TestUnitReporter.executable = "bin/test" + +Minitest.run_via = :rails + +require "active_support/testing/autorun" diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb new file mode 100644 index 0000000000..a083914109 --- /dev/null +++ b/railties/lib/rails/secrets.rb @@ -0,0 +1,111 @@ +require "yaml" + +module Rails + # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘 + class Secrets # :nodoc: + class MissingKeyError < RuntimeError + def initialize + super(<<-end_of_message.squish) + Missing encryption key to decrypt secrets with. + Ask your team for your master key and put it in ENV["RAILS_MASTER_KEY"] + end_of_message + end + end + + @read_encrypted_secrets = false + @root = File # Wonky, but ensures `join` uses the current directory. + + class << self + attr_writer :root + attr_accessor :read_encrypted_secrets + + def parse(paths, env:) + paths.each_with_object(Hash.new) do |path, all_secrets| + require "erb" + + secrets = YAML.load(ERB.new(preprocess(path)).result) || {} + all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"] + all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env] + end + end + + def generate_key + cipher = new_cipher + SecureRandom.hex(cipher.key_len)[0, cipher.key_len] + end + + def key + ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key + end + + def encrypt(text) + cipher(:encrypt, text) + end + + def decrypt(data) + cipher(:decrypt, data) + end + + def read + decrypt(IO.binread(path)) + end + + def write(contents) + IO.binwrite("#{path}.tmp", encrypt(contents)) + FileUtils.mv("#{path}.tmp", path) + end + + def read_for_editing + tmp_path = File.join(Dir.tmpdir, File.basename(path)) + IO.binwrite(tmp_path, read) + + yield tmp_path + + write(IO.binread(tmp_path)) + ensure + FileUtils.rm(tmp_path) if File.exist?(tmp_path) + end + + private + def handle_missing_key + raise MissingKeyError + end + + def read_key_file + if File.exist?(key_path) + IO.binread(key_path).strip + end + end + + def key_path + @root.join("config", "secrets.yml.key") + end + + def path + @root.join("config", "secrets.yml.enc").to_s + end + + def preprocess(path) + if path.end_with?(".enc") + if @read_encrypted_secrets + decrypt(IO.binread(path)) + else + "" + end + else + IO.read(path) + end + end + + def new_cipher + OpenSSL::Cipher.new("aes-256-cbc") + end + + def cipher(mode, data) + cipher = new_cipher.public_send(mode) + cipher.key = key + cipher.update(data) << cipher.final + end + end + end +end diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index ba1697186e..cb569be58b 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -17,6 +17,7 @@ STATS_DIRECTORIES = [ %w(Mailer\ tests test/mailers), %w(Job\ tests test/jobs), %w(Integration\ tests test/integration), + %w(System\ tests test/system), ].collect do |name, dir| [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ] end.select { |name, dir| File.directory?(dir) } diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 5fda160012..09931c108a 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -11,13 +11,19 @@ require "rails/generators/test_case" require "active_support/testing/autorun" +if defined?(Capbyara) + require "action_dispatch/system_test_case" +end + if defined?(ActiveRecord::Base) ActiveRecord::Migration.maintain_test_schema! - class ActiveSupport::TestCase - include ActiveRecord::TestFixtures - self.fixture_path = "#{Rails.root}/test/fixtures/" - self.file_fixture_path = fixture_path + "files" + module ActiveSupport + class TestCase + include ActiveRecord::TestFixtures + self.fixture_path = "#{Rails.root}/test/fixtures/" + self.file_fixture_path = fixture_path + "files" + end end ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path @@ -27,6 +33,8 @@ if defined?(ActiveRecord::Base) end end +# :enddoc: + class ActionController::TestCase def before_setup # :nodoc: @routes = Rails.application.routes @@ -40,3 +48,12 @@ class ActionDispatch::IntegrationTest super end end + +if defined? Capybara + class ActionDispatch::SystemTestCase + def before_setup # :nodoc: + @routes = Rails.application.routes + super + end + end +end diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 4df3e7f0f2..e44fe78bbd 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -59,18 +59,18 @@ module Minitest options[:color] = true options[:output_inline] = true - options[:patterns] = opts.order! unless run_via[:rake] + options[:patterns] = opts.order! unless run_via.rake? end def self.rake_run(patterns) # :nodoc: - run_via[:rake] = true + self.run_via = :rake unless run_via.set? ::Rails::TestRequirer.require_files(patterns) autorun end module RunRespectingRakeTestopts def run(args = []) - if run_via[:rake] + if run_via.rake? args = Shellwords.split(ENV["TESTOPTS"] || "") end @@ -87,7 +87,7 @@ module Minitest # If run via `ruby` we've been passed the files to run directly, or if run # via `rake` then they have already been eagerly required. - unless run_via[:ruby] || run_via[:rake] + unless run_via.ruby? || run_via.rake? ::Rails::TestRequirer.require_files(options[:patterns]) end @@ -102,7 +102,33 @@ module Minitest reporter << ::Rails::TestUnitReporter.new(options[:io], options) end - mattr_accessor(:run_via) { Hash.new } + def self.run_via=(runner) + if run_via.set? + raise ArgumentError, "run_via already assigned" + else + run_via.runner = runner + end + end + + class RunVia + attr_accessor :runner + alias set? runner + + # Backwardscompatibility with Rails 5.0 generated plugin test scripts. + def []=(runner, *) + @runner = runner + end + + def ruby? + runner == :ruby + end + + def rake? + runner == :rake + end + end + + mattr_reader(:run_via) { RunVia.new } end # Put Rails as the first plugin minitest initializes so other plugins diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index 746120e6a1..9cc3f73a9c 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -11,6 +11,7 @@ module Rails fixture_replacement: nil c.integration_tool :test_unit + c.system_tests :test_unit end initializer "test_unit.line_filtering" do diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 4c157c1262..4dde3d3c97 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -47,4 +47,9 @@ namespace :test do $: << "test" Minitest.rake_run(["test/controllers", "test/mailers", "test/functional"]) end + + task system: "test:prepare" do + $: << "test" + Minitest.rake_run(["test/system"]) + end end diff --git a/railties/test/application/help_test.rb b/railties/test/application/help_test.rb new file mode 100644 index 0000000000..0c3fe8bfa3 --- /dev/null +++ b/railties/test/application/help_test.rb @@ -0,0 +1,23 @@ +require "isolation/abstract_unit" + +class HelpTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "command works" do + output = Dir.chdir(app_path) { `bin/rails help` } + assert_match "The most common rails commands are", output + end + + test "short-cut alias works" do + output = Dir.chdir(app_path) { `bin/rails -h` } + assert_match "The most common rails commands are", output + end +end diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index c515e2b270..6742da20cc 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -263,7 +263,10 @@ module ApplicationTests assert_equal "WIN", last_response.body end - { "development" => "baz", "production" => "bar" }.each do |mode, expected| + { + "development" => ["baz", "http://www.apple.com", "/dashboard"], + "production" => ["bar", "http://www.microsoft.com", "/profile"] + }.each do |mode, (expected_action, expected_url, expected_mapping)| test "reloads routes when configuration is changed in #{mode}" do controller :foo, <<-RUBY class FooController < ApplicationController @@ -274,12 +277,40 @@ module ApplicationTests def baz render plain: "baz" end + + def custom + render plain: custom_url + end + + def mapping + render plain: url_for(User.new) + end + end + RUBY + + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false + end end RUBY app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#bar' + get 'custom', to: 'foo#custom' + get 'mapping', to: 'foo#mapping' + + direct(:custom) { "http://www.microsoft.com" } + resolve("User") { "/profile" } end RUBY @@ -288,16 +319,33 @@ module ApplicationTests get "/foo" assert_equal "bar", last_response.body + get "/custom" + assert_equal "http://www.microsoft.com", last_response.body + + get "/mapping" + assert_equal "/profile", last_response.body + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#baz' + get 'custom', to: 'foo#custom' + get 'mapping', to: 'foo#mapping' + + direct(:custom) { "http://www.apple.com" } + resolve("User") { "/dashboard" } end RUBY sleep 0.1 get "/foo" - assert_equal expected, last_response.body + assert_equal expected_action, last_response.body + + get "/custom" + assert_equal expected_url, last_response.body + + get "/mapping" + assert_equal expected_mapping, last_response.body end end @@ -358,6 +406,14 @@ module ApplicationTests def index render plain: "foo" end + + def custom + render plain: custom_url + end + + def mapping + render plain: url_for(User.new) + end end RUBY @@ -369,6 +425,21 @@ module ApplicationTests end RUBY + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false + end + end + RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' @@ -389,6 +460,12 @@ module ApplicationTests Rails.application.routes.draw do get 'foo', to: 'foo#index' get 'bar', to: 'bar#index' + + get 'custom', to: 'foo#custom' + direct(:custom) { 'http://www.apple.com' } + + get 'mapping', to: 'foo#mapping' + resolve('User') { '/profile' } end RUBY @@ -402,6 +479,14 @@ module ApplicationTests assert_equal "bar", last_response.body assert_equal "/bar", Rails.application.routes.url_helpers.bar_path + get "/custom" + assert_equal "http://www.apple.com", last_response.body + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url + + get "/mapping" + assert_equal "/profile", last_response.body + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' @@ -419,6 +504,18 @@ module ApplicationTests assert_raises NoMethodError do assert_equal "/bar", Rails.application.routes.url_helpers.bar_path end + + get "/custom" + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url + end + + get "/mapping" + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) + end end test "named routes are cleared when reloading" do @@ -440,19 +537,41 @@ module ApplicationTests end RUBY + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false + end + end + RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':locale/foo', to: 'foo#index', as: 'foo' + get 'users', to: 'foo#users', as: 'users' + direct(:microsoft) { 'http://www.microsoft.com' } + resolve('User') { '/profile' } end RUBY get "/en/foo" assert_equal "foo", last_response.body assert_equal "/en/foo", Rails.application.routes.url_helpers.foo_path(locale: "en") + assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':locale/bar', to: 'bar#index', as: 'foo' + get 'users', to: 'foo#users', as: 'users' + direct(:apple) { 'http://www.apple.com' } end RUBY @@ -464,6 +583,12 @@ module ApplicationTests get "/en/bar" assert_equal "bar", last_response.body assert_equal "/en/bar", Rails.application.routes.url_helpers.foo_path(locale: "en") + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.apple_url + assert_equal "/users", Rails.application.routes.url_helpers.polymorphic_path(User.new) + + assert_raises NoMethodError do + assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url + end end test "resource routing with irregular inflection" do @@ -493,5 +618,63 @@ module ApplicationTests get "/yazilar" assert_equal 200, last_response.status end + + test "reloading routes removes methods and doesn't undefine them" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + end + RUBY + + app_file "app/models/url_helpers.rb", <<-RUBY + module UrlHelpers + def foo_path + "/foo" + end + end + RUBY + + app_file "app/models/context.rb", <<-RUBY + class Context + include UrlHelpers + include Rails.application.routes.url_helpers + end + RUBY + + controller "url", <<-RUBY + class UrlController < ApplicationController + def index + context = Context.new + render plain: context.foo_path + end + end + RUBY + + get "/url" + assert_equal "/foo", last_response.body + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + get '/bar', to: 'foo#index', as: 'foo' + end + RUBY + + Rails.application.reload_routes! + + get "/url" + assert_equal "/bar", last_response.body + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + end + RUBY + + Rails.application.reload_routes! + + get "/url" + assert_equal "/foo", last_response.body + end end end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index d3d5b6d6dd..e773b52dbb 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -15,6 +15,16 @@ module ApplicationTests teardown_app end + def test_run_via_backwardscompatibility + require "rails/test_unit/minitest_plugin" + + assert_nothing_raised do + Minitest.run_via[:ruby] = true + end + + assert_predicate Minitest.run_via, :ruby? + end + def test_run_single_file create_test_file :models, "foo" create_test_file :models, "bar" @@ -60,16 +70,18 @@ module ApplicationTests end def test_run_units - skip "we no longer have the concept of unit tests. Just different directories..." create_test_file :models, "foo" create_test_file :helpers, "bar_helper" create_test_file :unit, "baz_unit" create_test_file :controllers, "foobar_controller" - run_test_units_command.tap do |output| - assert_match "FooTest", output - assert_match "BarHelperTest", output - assert_match "BazUnitTest", output - assert_match "3 runs, 3 assertions, 0 failures", output + + Dir.chdir(app_path) do + `bin/rails test:units`.tap do |output| + assert_match "FooTest", output + assert_match "BarHelperTest", output + assert_match "BazUnitTest", output + assert_match "3 runs, 3 assertions, 0 failures", output + end end end @@ -107,16 +119,18 @@ module ApplicationTests end def test_run_functionals - skip "we no longer have the concept of functional tests. Just different directories..." create_test_file :mailers, "foo_mailer" create_test_file :controllers, "bar_controller" create_test_file :functional, "baz_functional" create_test_file :models, "foo" - run_test_functionals_command.tap do |output| - assert_match "FooMailerTest", output - assert_match "BarControllerTest", output - assert_match "BazFunctionalTest", output - assert_match "3 runs, 3 assertions, 0 failures", output + + Dir.chdir(app_path) do + `bin/rails test:functionals`.tap do |output| + assert_match "FooMailerTest", output + assert_match "BarControllerTest", output + assert_match "BazFunctionalTest", output + assert_match "3 runs, 3 assertions, 0 failures", output + end end end diff --git a/railties/test/application/version_test.rb b/railties/test/application/version_test.rb new file mode 100644 index 0000000000..6b419ae7ae --- /dev/null +++ b/railties/test/application/version_test.rb @@ -0,0 +1,24 @@ +require "isolation/abstract_unit" +require "rails/gem_version" + +class VersionTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "command works" do + output = Dir.chdir(app_path) { `bin/rails version` } + assert_equal "Rails #{Rails.gem_version}\n", output + end + + test "short-cut alias works" do + output = Dir.chdir(app_path) { `bin/rails -v` } + assert_equal "Rails #{Rails.gem_version}\n", output + end +end diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index c54d9cc599..bdef1798f8 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -117,7 +117,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase public/apple-touch-icon-precomposed.png public/apple-touch-icon.png public/favicon.icon - vendor/package.json + package.json ) end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index ce29d93d6e..986afb6d2a 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -42,6 +42,7 @@ DEFAULT_APP_FILES = %w( test/helpers test/mailers test/integration + test/system vendor tmp tmp/cache @@ -334,12 +335,13 @@ class AppGeneratorTest < Rails::Generators::TestCase end assert_file "config/environments/production.rb" do |content| assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) + assert_match(/^ config\.read_encrypted_secrets = true/, content) end end def test_generator_defaults_to_puma_version run_generator [destination_root] - assert_gem "puma", "'~> 3.0'" + assert_gem "puma", "'~> 3.7'" end def test_generator_if_skip_puma_is_given @@ -421,7 +423,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_if_skip_yarn_is_given run_generator [destination_root, "--skip-yarn"] - assert_no_file "vendor/package.json" + assert_no_file "package.json" assert_no_file "bin/yarn" end @@ -496,21 +498,21 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_for_yarn run_generator([destination_root]) - assert_file "vendor/package.json", /dependencies/ + assert_file "package.json", /dependencies/ assert_file "config/initializers/assets.rb", /node_modules/ end def test_generator_for_yarn_skipped run_generator([destination_root, "--skip-yarn"]) - assert_no_file "vendor/package.json" + assert_no_file "package.json" assert_file "config/initializers/assets.rb" do |content| assert_no_match(/node_modules/, content) end assert_file ".gitignore" do |content| - assert_no_match(/vendor\/node_modules/, content) - assert_no_match(/vendor\/yarn-error\.log/, content) + assert_no_match(/node_modules/, content) + assert_no_match(/yarn-error\.log/, content) end end @@ -805,8 +807,26 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_equal 4, @sequence_step end - private + def test_system_tests_directory_generated + run_generator + assert_file("test/system/.keep") + assert_directory("test/system") + end + + def test_system_tests_are_not_generated_on_system_test_skip + run_generator [destination_root, "--skip-system-test"] + + assert_no_directory("test/system") + end + + def test_system_tests_are_not_generated_on_test_skip + run_generator [destination_root, "--skip-test"] + + assert_no_directory("test/system") + end + + private def stub_rails_application(root) Rails.application.config.root = root Rails.application.class.stub(:name, "Myapp") do diff --git a/railties/test/generators/encrypted_secrets_generator_test.rb b/railties/test/generators/encrypted_secrets_generator_test.rb new file mode 100644 index 0000000000..747abf19ed --- /dev/null +++ b/railties/test/generators/encrypted_secrets_generator_test.rb @@ -0,0 +1,42 @@ +require "generators/generators_test_helper" +require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" + +class EncryptedSecretsGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + + def setup + super + cd destination_root + end + + def test_generates_key_file_and_encrypted_secrets_file + run_generator + + assert_file "config/secrets.yml.key", /[\w\d]+/ + + assert File.exist?("config/secrets.yml.enc") + assert_no_match(/production:\n# external_api_key: [\w\d]+/, IO.binread("config/secrets.yml.enc")) + assert_match(/production:\n# external_api_key: [\w\d]+/, Rails::Secrets.read) + end + + def test_appends_to_gitignore + FileUtils.touch(".gitignore") + + run_generator + + assert_file ".gitignore", /config\/secrets.yml.key/, /(?!config\/secrets.yml.enc)/ + end + + def test_warns_when_ignore_is_missing + assert_match(/Add this to your ignore file/i, run_generator) + end + + def test_doesnt_generate_a_new_key_file_if_already_opted_in_to_encrypted_secrets + FileUtils.mkdir("config") + File.open("config/secrets.yml.enc", "w") { |f| f.puts "already secrety" } + + run_generator + + assert_no_file "config/secrets.yml.key" + end +end diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb index 904bade658..4444b3a56e 100644 --- a/railties/test/generators/generator_test.rb +++ b/railties/test/generators/generator_test.rb @@ -88,12 +88,12 @@ module Rails specifier_for = -> v { generator.send(:rails_version_specifier, Gem::Version.new(v)) } assert_equal "~> 4.1.13", specifier_for["4.1.13"] - assert_equal [">= 4.1.6.rc1", "< 4.2"], specifier_for["4.1.6.rc1"] + assert_equal "~> 4.1.6.rc1", specifier_for["4.1.6.rc1"] assert_equal ["~> 4.1.7", ">= 4.1.7.1"], specifier_for["4.1.7.1"] assert_equal ["~> 4.1.7", ">= 4.1.7.1.2"], specifier_for["4.1.7.1.2"] - assert_equal [">= 4.1.7.1.rc2", "< 4.2"], specifier_for["4.1.7.1.rc2"] - assert_equal [">= 4.2.0.beta1", "< 4.3"], specifier_for["4.2.0.beta1"] - assert_equal [">= 5.0.0.beta1", "< 5.1"], specifier_for["5.0.0.beta1"] + assert_equal ["~> 4.1.7", ">= 4.1.7.1.rc2"], specifier_for["4.1.7.1.rc2"] + assert_equal "~> 4.2.0.beta1", specifier_for["4.2.0.beta1"] + assert_equal "~> 5.0.0.beta1", specifier_for["5.0.0.beta1"] end end end diff --git a/railties/test/generators/integration_test_generator_test.rb b/railties/test/generators/integration_test_generator_test.rb index 8bcc02440a..9358b63bd4 100644 --- a/railties/test/generators/integration_test_generator_test.rb +++ b/railties/test/generators/integration_test_generator_test.rb @@ -3,10 +3,14 @@ require "rails/generators/rails/integration_test/integration_test_generator" class IntegrationTestGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper - arguments %w(integration) def test_integration_test_skeleton_is_created - run_generator + run_generator %w(integration) assert_file "test/integration/integration_test.rb", /class IntegrationTest < ActionDispatch::IntegrationTest/ end + + def test_namespaced_integration_test_skeleton_is_created + run_generator %w(iguchi/integration) + assert_file "test/integration/iguchi/integration_test.rb", /class Iguchi::IntegrationTest < ActionDispatch::IntegrationTest/ + end end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 2ad0ab55d3..8ec096e5c6 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -491,6 +491,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_directory "test/dummy/doc" assert_no_directory "test/dummy/test" assert_no_directory "test/dummy/vendor" + assert_no_directory "test/dummy/.git" end def test_skipping_test_files diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index e2b2acab0f..436fbd5d73 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -62,6 +62,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) end + # System tests + assert_file "test/system/product_lines_test.rb" do |test| + assert_match(/class ProductLinesTest < ApplicationSystemTestCase/, test) + end + # Views assert_no_file "app/views/layouts/product_lines.html.erb" diff --git a/railties/test/generators/system_test_generator_test.rb b/railties/test/generators/system_test_generator_test.rb new file mode 100644 index 0000000000..e8e561ec49 --- /dev/null +++ b/railties/test/generators/system_test_generator_test.rb @@ -0,0 +1,12 @@ +require "generators/generators_test_helper" +require "rails/generators/rails/system_test/system_test_generator" + +class SystemTestGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + arguments %w(user) + + def test_system_test_skeleton_is_created + run_generator + assert_file "test/system/users_test.rb", /class UsersTest < ApplicationSystemTestCase/ + end +end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 1902eac862..924503a522 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -22,6 +22,7 @@ require "active_support/core_ext/object/blank" require "active_support/testing/isolation" require "active_support/core_ext/kernel/reporting" require "tmpdir" +require "rails/secrets" module TestHelpers module Paths diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb new file mode 100644 index 0000000000..36e42cf1f9 --- /dev/null +++ b/railties/test/secrets_test.rb @@ -0,0 +1,108 @@ +require "abstract_unit" +require "isolation/abstract_unit" +require "rails/generators" +require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" +require "rails/secrets" + +class Rails::SecretsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + + @old_read_encrypted_secrets, Rails::Secrets.read_encrypted_secrets = + Rails::Secrets.read_encrypted_secrets, true + end + + def teardown + Rails::Secrets.read_encrypted_secrets = @old_read_encrypted_secrets + + teardown_app + end + + test "setting read to false skips parsing" do + Rails::Secrets.read_encrypted_secrets = false + + Dir.chdir(app_path) do + assert_equal Hash.new, Rails::Secrets.parse(%w( config/secrets.yml.enc ), env: "production") + end + end + + test "raises when reading secrets without a key" do + run_secrets_generator do + FileUtils.rm("config/secrets.yml.key") + + assert_raises Rails::Secrets::MissingKeyError do + Rails::Secrets.key + end + end + end + + test "reading with ENV variable" do + run_secrets_generator do + begin + old_key = ENV["RAILS_MASTER_KEY"] + ENV["RAILS_MASTER_KEY"] = IO.binread("config/secrets.yml.key").strip + FileUtils.rm("config/secrets.yml.key") + + assert_match "production:\n# external_api_key", Rails::Secrets.read + ensure + ENV["RAILS_MASTER_KEY"] = old_key + end + end + end + + test "reading from key file" do + run_secrets_generator do + File.binwrite("config/secrets.yml.key", "How do I know you feel it?") + + assert_equal "How do I know you feel it?", Rails::Secrets.key + end + end + + test "editing" do + run_secrets_generator do + decrypted_path = nil + + Rails::Secrets.read_for_editing do |tmp_path| + decrypted_path = tmp_path + + assert_match(/production:\n# external_api_key/, File.read(tmp_path)) + + File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.") + end + + assert_not File.exist?(decrypted_path) + assert_equal "Empty streets, empty nights. The Downtown Lights.", Rails::Secrets.read + end + end + + test "merging secrets with encrypted precedence" do + run_secrets_generator do + File.write("config/secrets.yml", <<-end_of_secrets) + test: + yeah_yeah: lets-go-walking-down-this-empty-street + end_of_secrets + + Rails::Secrets.write(<<-end_of_secrets) + test: + yeah_yeah: lets-walk-in-the-cool-evening-light + end_of_secrets + + Rails.application.config.root = app_path + Rails.application.instance_variable_set(:@secrets, nil) # Dance around caching 💃🕺 + assert_equal "lets-walk-in-the-cool-evening-light", Rails.application.secrets.yeah_yeah + end + end + + private + def run_secrets_generator + Dir.chdir(app_path) do + capture(:stdout) do + Rails::Generators::EncryptedSecretsGenerator.start + end + + yield + end + end +end |