# frozen_string_literal: true require 'test_helper' module Shipit class DeploySpecTest < ActiveSupport::TestCase setup do @app_dir = '/tmp/' @spec = DeploySpec::FileSystem.new(@app_dir, 'env') @spec.stubs(:load_config).returns({}) end test '#supports_fetch_deployed_revision? returns false by default' do refute @spec.supports_fetch_deployed_revision? end test '#supports_fetch_deployed_revision? returns true if steps are defined' do @spec.stubs(:load_config).returns('fetch' => ['curl --silent https://example.com/status/version']) assert @spec.supports_fetch_deployed_revision? end test '#supports_rollback? returns false by default' do refute @spec.supports_rollback? end test '#supports_rollback? returns true if steps are defined' do @spec.stubs(:load_config).returns('rollback' => { 'override' => ['rm -rf /usr /lib/nvidia-current/xorg/xorg'] }) assert @spec.supports_rollback? end test '#supports_rollback? returns true if stack is detected as capistrano' do @spec.expects(:capistrano?).returns(true) assert @spec.supports_rollback? end test '#dependencies_steps returns `dependencies.override` if present' do @spec.stubs(:load_config).returns('dependencies' => { 'override' => %w(foo bar baz) }) assert_equal %w(foo bar baz), @spec.dependencies_steps end test '#dependencies_steps returns `bundle install` if a `Gemfile` is present' do @spec.expects(:bundler?).returns(true).at_least_once @spec.expects(:bundle_install).returns(['bundle install']) assert_equal ['bundle install'], @spec.dependencies_steps end test "#dependencies_steps prepend and append pre and post steps" do @spec.stubs(:load_config).returns('dependencies' => { 'pre' => ['before'], 'post' => ['after'] }) @spec.expects(:bundler?).returns(true).at_least_once @spec.expects(:bundle_install).returns(['bundle install']) assert_equal ['before', 'bundle install', 'after'], @spec.dependencies_steps end test '#fetch_deployed_revision_steps! is unknown by default' do assert_raises DeploySpec::Error do @spec.fetch_deployed_revision_steps! end end test '#fetch_deployed_revision_steps use `fetch` is present' do @spec.stubs(:load_config).returns('fetch' => ['echo l33t']) assert_equal ['echo l33t'], @spec.fetch_deployed_revision_steps end test '#bundle_install return a sane default bundle install command' do @spec.stubs(:gemfile_lock_exists?).returns(true) command = %( bundle install --frozen --jobs 4 --path #{DeploySpec.bundle_path} --retry 2 --without=default:production:development:test:staging:benchmark:debug ).gsub(/\s+/, ' ').strip assert_equal command, @spec.bundle_install.last end test '#bundle_install use `dependencies.bundler.without` if present to build the --without argument' do @spec.stubs(:gemfile_lock_exists?).returns(true) @spec.stubs(:load_config).returns('dependencies' => { 'bundler' => { 'without' => %w(some custom groups) } }) command = %( bundle install --frozen --jobs 4 --path #{DeploySpec.bundle_path} --retry 2 --without=some:custom:groups ).gsub(/\s+/, ' ').strip assert_equal command, @spec.bundle_install.last end test '#bundle_install has --frozen option if Gemfile.lock is present' do @spec.stubs(:load_config).returns('dependencies' => { 'bundler' => { 'without' => %w(some custom groups) } }) @spec.stubs(:gemfile_lock_exists?).returns(true) assert @spec.bundle_install.last.include?('--frozen') end test '#bundle_install does not have --frozen option if Gemfile.lock is not present' do @spec.stubs(:load_config).returns('dependencies' => { 'bundler' => { 'without' => %w(some custom groups) } }) @spec.stubs(:gemfile_lock_exists?).returns(false) refute @spec.bundle_install.last.include?('--frozen') end test '#bundle_install does not have --frozen if overridden in shipit.yml' do @spec.stubs(:load_config).returns('dependencies' => { 'bundler' => { 'frozen' => false } }) @spec.stubs(:gemfile_lock_exists?).returns(true) refute @spec.bundle_install.last.include?('--frozen') end test "#provisioning_handler returns `provision.handler` if present" do @spec.stubs(:load_config).returns('provision' => { 'handler_name' => 'ExpectedProvisioningHandler' }) assert_equal "ExpectedProvisioningHandler", @spec.provisioning_handler_name end test '#deploy_steps returns `deploy.override` if present' do @spec.stubs(:load_config).returns('deploy' => { 'override' => %w(foo bar baz) }) assert_equal %w(foo bar baz), @spec.deploy_steps end test '#deploy_steps returns `cap $ENVIRONMENT deploy` if a `Capfile` is present' do @spec.expects(:bundler?).returns(true).at_least_once @spec.expects(:capistrano?).returns(true) assert_equal ['bundle exec cap $ENVIRONMENT deploy'], @spec.deploy_steps end test "#deploy_steps returns `kubernetes-deploy ` if `kubernetes` is present" do @spec.stubs(:load_config).returns( 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', }, ) assert_equal ["kubernetes-deploy --max-watch-seconds 900 foo bar"], @spec.deploy_steps end test "#deploy_steps `kubernetes` respects timeout false" do @spec.stubs(:load_config).returns( 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', 'timeout' => false, }, ) assert_equal ["kubernetes-deploy foo bar"], @spec.deploy_steps end test "#deploy_steps returns kubernetes-deploy command if both capfile and `kubernetes` are present" do @spec.stubs(:bundler?).returns(true) @spec.stubs(:capistrano?).returns(true) @spec.stubs(:load_config).returns( 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', }, ) assert_equal ["kubernetes-deploy --max-watch-seconds 900 foo bar"], @spec.deploy_steps end test "#deploy_steps returns kubernetes command if `kubernetes` is present and template_dir is set" do @spec.stubs(:load_config).returns( 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', 'template_dir' => 'k8s_templates/', }, ) assert_equal ["kubernetes-deploy --max-watch-seconds 900 --template-dir k8s_templates/ foo bar"], @spec.deploy_steps end test "#deploy_steps prepend and append pre and post steps" do @spec.stubs(:load_config).returns('deploy' => { 'pre' => ['before'], 'post' => ['after'] }) @spec.expects(:bundler?).returns(true).at_least_once @spec.expects(:capistrano?).returns(true) assert_equal ['before', 'bundle exec cap $ENVIRONMENT deploy', 'after'], @spec.deploy_steps end test '#deploy_steps raise a DeploySpec::Error! if it dont know how to deploy the app' do @spec.expects(:capistrano?).returns(false) assert_raise DeploySpec::Error do @spec.deploy_steps! end end test '#retries_on_deploy returns `deploy.retries` if present' do @spec.stubs(:load_config).returns('deploy' => { 'retries' => 5 }) assert_equal 5, @spec.retries_on_deploy end test '#retries_on_deploy returns a default value if `deploy.retries` is not present' do @spec.stubs(:load_config).returns('deploy' => {}) assert_nil @spec.retries_on_deploy end test '#rollback_steps returns `rollback.override` if present' do @spec.stubs(:load_config).returns('rollback' => { 'override' => %w(foo bar baz) }) assert_equal %w(foo bar baz), @spec.rollback_steps end test '#retries_on_rollback returns `rollback.retries` if present' do @spec.stubs(:load_config).returns('rollback' => { 'retries' => 5 }) assert_equal 5, @spec.retries_on_rollback end test '#retries_on_rollback returns a default value if `rollback.retries` is not present' do @spec.stubs(:load_config).returns('rollback' => {}) assert_nil @spec.retries_on_rollback end test '#rollback_steps returns `cap $ENVIRONMENT deploy:rollback` if a `Capfile` is present' do @spec.expects(:bundler?).returns(true).at_least_once @spec.expects(:capistrano?).returns(true) assert_equal ['bundle exec cap $ENVIRONMENT deploy:rollback'], @spec.rollback_steps end test "#rollback_steps prepend and append pre and post steps" do @spec.stubs(:load_config).returns('rollback' => { 'pre' => ['before'], 'post' => ['after'] }) @spec.expects(:bundler?).returns(true).at_least_once @spec.expects(:capistrano?).returns(true) assert_equal ['before', 'bundle exec cap $ENVIRONMENT deploy:rollback', 'after'], @spec.rollback_steps end test "#rollback_steps returns `kubernetes-deploy ` if `kubernetes` is present" do @spec.stubs(:load_config).returns( 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', }, ) assert_equal ["kubernetes-deploy --max-watch-seconds 900 foo bar"], @spec.rollback_steps end test "#rollback_steps returns kubernetes-deploy command when both capfile and `kubernetes` are present" do @spec.stubs(:bundler?).returns(true) @spec.stubs(:capistrano?).returns(true) @spec.stubs(:load_config).returns( 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', }, ) assert_equal ["kubernetes-deploy --max-watch-seconds 900 foo bar"], @spec.rollback_steps end test "#discover_task_definitions include a kubernetes restart command if `kubernetes` is present" do @spec.stubs(:load_config).returns( 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', }, ) tasks = { 'restart' => { 'action' => 'Restart application', 'description' => 'Simulates a rollout of Kubernetes deployments by using kubernetes-restart utility', 'steps' => ['kubernetes-restart foo bar --max-watch-seconds 900'], }, } assert_equal tasks, @spec.discover_task_definitions end test "#discover_task_definitions include the user defined restart command even if `kubernetes` is present" do tasks = { 'restart' => { 'action' => 'Restart application', 'description' => 'Simulates a rollout of Kubernetes deployments by using kubernetes-restart utility', 'steps' => ['kubernetes-restart something custom'], }, 'some-other-tasj' => { 'action' => 'Do something else', 'description' => 'Eat some chips!', 'steps' => ['echo chips'], }, } @spec.stubs(:load_config).returns( 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', }, 'tasks' => tasks, ) assert_equal tasks, @spec.discover_task_definitions end test '#machine_env returns an environment hash' do @spec.stubs(:load_config).returns('machine' => { 'environment' => { 'GLOBAL' => '1' } }) assert_equal({ 'GLOBAL' => '1' }, @spec.machine_env) end test '#load_config can grab the env-specific shipit.yml file' do config = {} config.expects(:exist?).returns(true) config.expects(:read).returns({ 'dependencies' => { 'override' => %w(foo bar baz) } }.to_yaml) spec = DeploySpec::FileSystem.new('.', 'staging') spec.expects(:file).with('shipit.staging.yml', root: true).returns(config) assert_equal %w(foo bar baz), spec.dependencies_steps end test '#load_config grabs the global shipit.yml file if there is no env-specific file' do not_config = {} not_config.expects(:exist?).returns(false) config = {} config.expects(:exist?).returns(true) config.expects(:read).returns({ 'dependencies' => { 'override' => %w(foo bar baz) } }.to_yaml) spec = DeploySpec::FileSystem.new('.', 'staging') spec.expects(:file).with('shipit.staging.yml', root: true).returns(not_config) spec.expects(:file).with('shipit.yml', root: true).returns(config) assert_equal %w(foo bar baz), spec.dependencies_steps end test '#gemspec gives the path of the repo gemspec if present' do spec = DeploySpec::FileSystem.new('foobar/', 'production') Dir.expects(:[]).with('foobar/*.gemspec').returns(['foobar/foobar.gemspec']) assert_equal 'foobar/foobar.gemspec', spec.gemspec end test '#gem? is true if a gemspec is present' do @spec.expects(:gemspec).returns('something') assert @spec.gem? end test '#gem? is false if there is no gemspec' do @spec.expects(:gemspec).returns(nil) refute @spec.gem? end test '#publish_gem first check if version tag have been created, and then invoke bundler release task' do @spec.stubs(:gemspec).returns('/tmp/shipit.gemspec') refute @spec.capistrano? assert_equal ['release-gem /tmp/shipit.gemspec'], @spec.deploy_steps end test '#setup_dot_py gives the path of the repo setup.py if present' do spec = DeploySpec::FileSystem.new('foobar/', 'production') assert_equal Pathname.new('foobar/setup.py'), spec.setup_dot_py end test '#egg? is true if a setup.py is present' do @spec.expects(:setup_dot_py).returns(Shipit::Engine.root.join('Gemfile')) assert @spec.egg? end test '#egg? is false if there is no setup.py' do @spec.expects(:setup_dot_py).returns(Shipit::Engine.root.join("tmp-#{SecureRandom.hex}")) refute @spec.egg? end test '#publish_egg first check if version tag have been created and then invoke setup.py upload' do file = Pathname.new('/tmp/fake_setup.py') file.write('foo') @spec.stubs(:setup_dot_py).returns(file) steps = [ 'assert-egg-version-tag /tmp/fake_setup.py', 'python setup.py register sdist', 'twine upload dist/*', ] assert_equal steps, @spec.deploy_steps end test '#cacheable returns a DeploySpec instance that can be serialized' do assert_instance_of DeploySpec::FileSystem, @spec assert_instance_of DeploySpec, @spec.cacheable config = { 'merge' => { 'require' => [], 'ignore' => [], 'revalidate_after' => nil, 'method' => nil, 'max_divergence' => { 'commits' => nil, 'age' => nil, }, }, 'ci' => { 'hide' => [], 'allow_failures' => [], 'require' => [], 'blocking' => [], }, 'machine' => { 'environment' => { 'BUNDLE_PATH' => @spec.bundle_path.to_s }, 'directory' => nil, 'cleanup' => true, }, 'review' => { 'checklist' => [], 'monitoring' => [], 'checks' => [] }, 'status' => { 'context' => nil, 'delay' => 0, }, 'dependencies' => { 'override' => [] }, 'plugins' => {}, 'provision' => { 'handler_name' => nil, }, 'deploy' => { 'override' => nil, 'variables' => [], 'max_commits' => 8, 'interval' => 0, 'retries' => nil, }, 'rollback' => { 'override' => nil, 'retries' => nil, }, 'fetch' => nil, 'tasks' => {}, } assert_equal config, @spec.cacheable.config end test "#deploy_variables returns an empty array by default" do assert_equal [], @spec.deploy_variables end test "#deploy_variables returns an array of VariableDefinition instances" do @spec.stubs(:load_config).returns('deploy' => { 'variables' => [{ 'name' => 'SAFETY_DISABLED', 'title' => 'Set to 1 to do dangerous things', 'default' => 0, }] }) assert_equal 1, @spec.deploy_variables.size variable_definition = @spec.deploy_variables.first assert_equal 'SAFETY_DISABLED', variable_definition.name end test "task definitions don't prepend bundle exec by default" do @spec.expects(:load_config).returns('tasks' => { 'restart' => { 'steps' => %w(foo) } }) definition = @spec.find_task_definition('restart') assert_equal ['foo'], definition.steps end test "task definitions don't bundle exec before serialization" do @spec.expects(:discover_task_definitions).returns('restart' => { 'steps' => %w(foo) }) @spec.expects(:bundler?).returns(true).at_least_once cached_spec = DeploySpec.load(DeploySpec.dump(@spec)) definition = cached_spec.find_task_definition('restart') assert_equal ['foo'], definition.steps end test "#task_definitions will always have definitions from shipit.yml take precedence over other modules" do # Create a subclass of `FileSystem` which we can include another module in without # affecting any other tests DuplicateCustomizedDeploySpec = Class.new(Shipit::DeploySpec::FileSystem) # Create the module we want to include in our new class # For this test case, we want to have the module create a task with the same # ID as defined from config module TestTaskDiscovery def discover_task_definitions { 'config_task' => { 'steps' => %w(bar) }, }.merge!(super) end end # Include the module in our new test class DuplicateCustomizedDeploySpec.include(TestTaskDiscovery) # Setup the spec as we would normally, but use the customized version @spec = DuplicateCustomizedDeploySpec.new(@app_dir, 'env') @spec.stubs(:load_config).returns( 'tasks' => { 'config_task' => { 'steps' => %w(foo) } }, ) tasks = @spec.task_definitions # Assert we get only the task from the config, not from the module assert_equal %w(config_task), tasks.map(&:id) assert_equal ["foo"], tasks.first.steps end test "#task_definitions returns comands from the config and other modules" do # Create a subclass of `FileSystem` which we can include another module in without # affecting any other tests CustomizedDeploySpec = Class.new(Shipit::DeploySpec::FileSystem) # Create the module we want to include in our new class # This module demonstrates how to have new tasks to be appended to the task list module TestTaskDiscovery def discover_task_definitions { 'module_task' => { 'steps' => %w(bar) }, }.merge(super) end end # Include the module in our new test class CustomizedDeploySpec.include(TestTaskDiscovery) # Setup the spec as we would normally, but use the customized version @spec = CustomizedDeploySpec.new(@app_dir, 'env') @spec.stubs(:load_config).returns( 'tasks' => { 'config_task' => { 'steps' => %w(foo) } }, 'kubernetes' => { 'namespace' => 'foo', 'context' => 'bar', 'timeout' => '20m', }, ) tasks = @spec.task_definitions # Assert we get tasks from all three sources: config, shipit-engine defined modules, and # "third party" modules assert_equal %w(config_task module_task restart), tasks.map(&:id).sort module_task = tasks.find { |t| t.id == "config_task" } assert_equal ["foo"], module_task.steps config_task = tasks.find { |t| t.id == "module_task" } assert_equal ["bar"], config_task.steps restart_task = tasks.find { |t| t.id == "restart" } assert_equal ["kubernetes-restart foo bar --max-watch-seconds 1200"], restart_task.steps end test "task definitions returns an array of VariableDefinition instances" do @spec.expects(:load_config).returns('tasks' => { 'restart' => { 'variables' => [ { 'name' => 'SAFETY_DISABLED', 'title' => 'Set to 1 to do dangerous things', 'default' => 0, }, { 'name' => 'FOO', 'title' => 'Set to 0 to foo', 'default' => 1, }, ], 'steps' => %w(foo), } }) assert_equal 2, @spec.task_definitions.first.variables.size variable_definition = @spec.task_definitions.first.variables.first assert_equal 'SAFETY_DISABLED', variable_definition.name end test "task definitions returns an empty array by default" do assert_equal [], @spec.task_definitions end test "#review_checklist returns an array" do @spec.expects(:load_config).returns('review' => { 'checklist' => %w(foo bar) }) assert_equal %w(foo bar), @spec.review_checklist end test "#review_checklist returns an empty array if the section is missing" do assert_equal [], @spec.review_checklist end test "#review_monitoring returns an array of hashes" do @spec.expects(:load_config).returns('review' => { 'monitoring' => [ { 'image' => 'http://example.com/foo.png', 'width' => 200, 'height' => 400 }, { 'iframe' => 'http://example.com/', 'width' => 200, 'height' => 400 }, ] }) assert_equal [ { 'image' => 'http://example.com/foo.png', 'width' => 200, 'height' => 400 }, { 'iframe' => 'http://example.com/', 'width' => 200, 'height' => 400 }, ], @spec.review_monitoring end test "#review_monitoring returns an empty array if the section is missing" do assert_equal [], @spec.review_monitoring end test "#hidden_statuses is empty by default" do assert_equal [], @spec.hidden_statuses end test "#hidden_statuses is an array even if the value is a string" do @spec.expects(:load_config).returns('ci' => { 'hide' => 'ci/circleci' }) assert_equal %w(ci/circleci), @spec.hidden_statuses end test "#hidden_statuses is an array even if the value is present" do @spec.expects(:load_config).returns('ci' => { 'hide' => %w(ci/circleci ci/jenkins) }) assert_equal %w(ci/circleci ci/jenkins), @spec.hidden_statuses end test "#required_statuses automatically includes #blocking_statuses" do @spec.expects(:load_config).returns( 'ci' => { 'require' => %w(ci/circleci), 'blocking' => %w(soc/compliance), }, ) assert_equal %w(ci/circleci soc/compliance), @spec.required_statuses end test "merge_request_merge_method defaults to `nil`" do @spec.expects(:load_config).returns({}) assert_nil @spec.merge_request_merge_method end test "merge_request_merge_method returns `merge.method`" do @spec.expects(:load_config).returns( 'merge' => { 'method' => 'squash', }, ) assert_equal 'squash', @spec.merge_request_merge_method end test "merge_request_merge_method returns `nil` if `merge.method` is invalid" do @spec.expects(:load_config).returns( 'merge' => { 'method' => 'squashing', }, ) assert_nil @spec.merge_request_merge_method end test "merge_request_ignored_statuses defaults to the union of ci.hide and ci.allow_failures" do @spec.expects(:load_config).returns( 'ci' => { 'hide' => %w(ci/circleci ci/jenkins), 'allow_failures' => %w(ci/circleci ci/travis), }, ) assert_equal %w(ci/circleci ci/jenkins ci/travis).sort, @spec.merge_request_ignored_statuses.sort end test "merge_request_ignored_statuses defaults to empty if `merge.require` is present" do @spec.expects(:load_config).returns( 'merge' => { 'require' => 'bar', }, 'ci' => { 'hide' => %w(ci/circleci ci/jenkins), 'allow_failures' => %w(ci/circleci ci/travis), }, ) assert_equal [], @spec.merge_request_ignored_statuses end test "merge_request_ignored_statuses returns `merge.ignore` if present" do @spec.expects(:load_config).returns( 'merge' => { 'ignore' => 'bar', }, 'ci' => { 'hide' => %w(ci/circleci ci/jenkins), 'allow_failures' => %w(ci/circleci ci/travis), }, ) assert_equal ['bar'], @spec.merge_request_ignored_statuses end test "merge_request_required_statuses defaults to ci.require" do @spec.expects(:load_config).returns( 'ci' => { 'require' => %w(ci/circleci ci/jenkins), }, ) assert_equal %w(ci/circleci ci/jenkins), @spec.merge_request_required_statuses end test "merge_request_required_statuses defaults to empty if `merge.ignore` is present" do @spec.expects(:load_config).returns( 'merge' => { 'ignore' => 'bar', }, 'ci' => { 'require' => %w(ci/circleci ci/jenkins), }, ) assert_equal [], @spec.merge_request_required_statuses end test "merge_request_required_statuses returns `merge.require` if present" do @spec.expects(:load_config).returns( 'merge' => { 'require' => 'bar', }, 'ci' => { 'hide' => %w(ci/circleci ci/jenkins), 'allow_failures' => %w(ci/circleci ci/travis), }, ) assert_equal ['bar'], @spec.merge_request_required_statuses end test "revalidate_merge_requests_after defaults to `nil" do @spec.expects(:load_config).returns({}) assert_nil @spec.revalidate_merge_requests_after end test "revalidate_merge_requests_after defaults to `nil` if `merge.timeout` cannot be parsed" do @spec.expects(:load_config).returns( 'merge' => { 'revalidate_after' => 'ALSKhfjsdkf', }, ) assert_nil @spec.revalidate_merge_requests_after end test "revalidate_after returns `merge.revalidate_after` if present" do @spec.expects(:load_config).returns( 'merge' => { 'revalidate_after' => '5m30s', }, ) assert_equal 330, @spec.revalidate_merge_requests_after.to_i end test "#file is impacted by `machine.directory`" do subdir = '/foo/bar' @spec.stubs(:load_config).returns('machine' => { 'directory' => subdir }) assert_instance_of Pathname, @spec.file('baz') assert_equal File.join(@app_dir, subdir, 'baz'), @spec.file('baz').to_s end test "#clear_working_directory? returns true by default" do assert_predicate @spec, :clear_working_directory? end test 'bundler installs take priority over npm installs' do @spec.expects(:discover_package_json).never @spec.stubs(:discover_bundler).returns(['fake bundler task']).once assert_equal ['fake bundler task'], @spec.dependencies_steps end test 'Gems deploys take priority over npm deploys' do @spec.expects(:discover_npm_package).never @spec.stubs(:discover_gem).returns(['fake gem task']).once assert_equal ['fake gem task'], @spec.deploy_steps end test 'lerna monorepos take priority over solo npm deploys' do @spec.expects(:discover_npm_package).never @spec.stubs(:discover_lerna_packages).returns(['fake monorepo task']).once assert_equal ['fake monorepo task'], @spec.deploy_steps end test '#lerna? is false if there is no lerna.json' do @spec.expects(:lerna_json).returns(Shipit::Engine.root.join("tmp-#{SecureRandom.hex}")) refute @spec.lerna? end test '#npm? is false if there is no package.json' do @spec.expects(:package_json).returns(Shipit::Engine.root.join("tmp-#{SecureRandom.hex}")) refute @spec.npm? end test '#npm? is true if npm package is public' do file = Pathname.new('/tmp/fake_package.json') file.write('{"private": false}') @spec.expects(:package_json).returns(file) assert @spec.npm? end test '#npm? is false if npm package is private' do file = Pathname.new('/tmp/fake_package.json') file.write('{"private": true}') @spec.expects(:package_json).returns(file) refute @spec.npm? end test 'lerna monorepos have a checklist' do @spec.stubs(:lerna?).returns(true).at_least_once @spec.stubs(:lerna_config).returns('lerna' => '2.0.0', 'version' => '1.0.0') assert_match(/lerna publish --skip-npm/, @spec.review_checklist[0]) end test 'npm packages have a checklist' do @spec.stubs(:npm?).returns(true).at_least_once assert_match(/npm version/, @spec.review_checklist[0]) end test '#lerna_version returns the monorepo root version number' do file = Pathname.new('/tmp/fake_lerna.json') file.write('{"version": "1.0.0-beta.1"}') @spec.expects(:lerna_json).at_least_once.returns(file) assert_equal '1.0.0-beta.1', @spec.lerna_version end test '#lerna_lerna returns the lerna version specified' do file = Pathname.new('/tmp/fake_lerna.json') file.write('{"version": "1.0.0-beta.1", "lerna": "3.13.3"}') @spec.expects(:lerna_json).at_least_once.returns(file) assert_equal Gem::Version.new('3.13.3'), @spec.lerna_lerna end test '#package_version returns the version number' do file = Pathname.new('/tmp/fake_package.json') file.write('{"version": "1.0.0-beta.1"}') @spec.expects(:package_json).returns(file) assert_equal '1.0.0-beta.1', @spec.package_version end test '#dist_tag returns "latest" if the version does not contains a standard pre-release tag' do assert_equal 'latest', @spec.dist_tag('1.0.0') assert_equal 'latest', @spec.dist_tag('1.0.0-shopifyv4') end test '#dist_tag returns "next" if the version contains a pre-release tag' do assert_equal 'next', @spec.dist_tag('1.0.0-alpha.1') assert_equal 'next', @spec.dist_tag('1.0.0-beta') assert_equal 'next', @spec.dist_tag('1.0.0-rc.3') assert_equal 'next', @spec.dist_tag('1.0.0-next') end test '#dependencies_steps returns lerna setup if a `lerna.json` is present' do @spec.expects(:lerna?).returns(true).at_least_once assert_equal ['npm install --no-progress', 'node_modules/.bin/lerna bootstrap'], @spec.dependencies_steps end test '#dependencies_steps returns `npm install` if a `package.json` is present' do @spec.expects(:npm?).returns(true).at_least_once assert_equal ['npm install --no-progress'], @spec.dependencies_steps end test '#publish_lerna_packages checks if independent version tags exist, and then invokes lerna deploy script' do @spec.stubs(:lerna?).returns(true) @spec.stubs(:lerna_config).returns('lerna' => '2.0.0', 'version' => 'independent') assert_equal 'assert-lerna-independent-version-tags', @spec.deploy_steps[0] assert_equal 'publish-lerna-independent-packages', @spec.deploy_steps[1] end test '#publish_lerna_packages checks if fixed version tag exists, and then invokes lerna deploy script' do @spec.stubs(:lerna?).returns(true) @spec.stubs(:lerna_config).returns('lerna' => '2.0.0', 'version' => '1.0.0') assert_equal 'assert-lerna-fixed-version-tag', @spec.deploy_steps[0] assert_equal 'node_modules/.bin/lerna publish --yes --skip-git --repo-version 1.0.0 --force-publish=* --npm-tag latest --npm-client=npm --skip-npm=false', @spec.deploy_steps[1] end test '#publish_lerna_packages checks if a newer version of lerna is used, and will then use the new publish syntax, correctly setting the `latest` dist tag' do @spec.stubs(:lerna?).returns(true) @spec.stubs(:lerna_config).returns('lerna' => '3.0.0', 'version' => '1.0.0') assert_equal 'assert-lerna-fixed-version-tag', @spec.deploy_steps[0] assert_equal 'node_modules/.bin/lerna publish from-git --yes --dist-tag latest', @spec.deploy_steps[1] end test '#publish_lerna_packages checks if a newer version of lerna is used, and will then use the new publish syntax, correctly setting the `next` dist tag' do @spec.stubs(:lerna?).returns(true) @spec.stubs(:lerna_config).returns('lerna' => '3.0.0', 'version' => 'v1.3.1-alpha.2') assert_equal 'assert-lerna-fixed-version-tag', @spec.deploy_steps[0] assert_equal 'node_modules/.bin/lerna publish from-git --yes --dist-tag next', @spec.deploy_steps[1] end test '#enforce_publish_config? is false when Shipit.enforce_publish_config is nil' do Shipit.stubs(:enforce_publish_config).returns(nil) refute @spec.enforce_publish_config? end test '#enforce_publish_config? is false when Shipit.enforce_publish_config is 0' do Shipit.stubs(:enforce_publish_config).returns('0') refute @spec.enforce_publish_config? end test '#enforce_publish_config? is true when Shipit.enforce_publish_config is 1' do Shipit.stubs(:enforce_publish_config).returns('1') assert @spec.enforce_publish_config? end test '#valid_publish_config? is false when enforce_publish_config? is true and publishConfig is missing from package.json' do Shipit.stubs(:private_npm_registry).returns('some_private_registry') @spec.stubs(:enforce_publish_config?).returns(true) @spec.stubs(:publish_config_access).returns('restricted') package_json = Pathname.new('/tmp/fake_package.json') package_json.write('{"name": "foo"}') @spec.expects(:package_json).returns(package_json) refute @spec.valid_publish_config? end test '#valid_publish_config? is true when enforce_publish_config? is true and publishConfig.access is public' do Shipit.stubs(:private_npm_registry).returns('some_private_registry') @spec.stubs(:enforce_publish_config?).returns(true) @spec.stubs(:publish_config_access).returns('public') @spec.stubs(:publish_config).returns('something') assert @spec.valid_publish_config? end test '#valid_publish_config? is true when shipit does not enforce a publishConfig' do @spec.stubs(:lerna?).returns(true) @spec.stubs(:lerna_config).returns('lerna' => '2.0.0', 'version' => '1.0.0') assert @spec.valid_publish_config? end test '#publish_config returns publishConfig from package.json' do package_json = Pathname.new('/tmp/fake_package.json') package_json.write('{"publishConfig": "foo"}') @spec.expects(:package_json).returns(package_json) assert_equal "foo", @spec.publish_config end test '#valid_publish_config_access? is false when publishConfig.access is invalid' do @spec.stubs(:publish_config_access).returns('foo') refute @spec.valid_publish_config_access? end test '#valid_publish_config_access? is true when publishConfig.access is public or restricted' do @spec.stubs(:publish_config_access).returns('public') assert @spec.valid_publish_config_access? @spec.stubs(:publish_config_access).returns('restricted') assert @spec.valid_publish_config_access? end test '#publish_config_access is restricted when enforce_publish_config? is true and publishConfig is missing' do package_json = Pathname.new('/tmp/fake_package.json') package_json.write('{"name": "@shopify/foo"}') @spec.stubs(:enforce_publish_config?).returns(true) @spec.expects(:package_json).returns(package_json) assert_equal 'restricted', @spec.publish_config_access end test '#publish_config_access is public when enforce_publish_config? is false and publishConfig is missing' do package_json = Pathname.new('/tmp/fake_package.json') package_json.write('{"name": "@shopify/foo"}') @spec.stubs(:enforce_publish_config?).returns(false) @spec.expects(:package_json).returns(package_json) assert_equal 'public', @spec.publish_config_access end test '#publish_config_access returns publishConfig.access from package.json when enforce_publish_config? is true' do package_json = Pathname.new('/tmp/fake_package.json') package_json.write('{ "name": "@shopify/foo", "publishConfig": { "access": "foo" } }') @spec.stubs(:enforce_publish_config?).returns(false) @spec.expects(:package_json).returns(package_json) assert_equal 'foo', @spec.publish_config_access end test "#scoped_package? is false when Shipit.npm_org_scope is not set and the package is private" do Shipit.stubs(:npm_org_scope).returns(nil) @spec.stubs(:publish_config_access).returns('restricted') refute @spec.scoped_package? end test "#scoped_package? is true when Shipit.npm_org_scope is set and package_name starts with scope and the package is private" do Shipit.stubs(:npm_org_scope).returns('@shopify') @spec.stubs(:publish_config_access).returns('restricted') @spec.stubs(:package_name).returns('@shopify/polaris') assert @spec.scoped_package? end test "#private_scoped_package? is false when private packages are not scoped" do @spec.stubs(:scoped_package?).returns(false) @spec.stubs(:publish_config_access).returns("restricted") refute @spec.private_scoped_package? end test "#private_scoped_package? is true when private packages are scoped" do @spec.stubs(:scoped_package?).returns(true) @spec.stubs(:publish_config_access).returns("restricted") assert @spec.private_scoped_package? end test '#publish_npm_package checks if version tag exists, and then invokes npm deploy script' do @spec.stubs(:npm?).returns(true) @spec.stubs(:package_version).returns('1.0.0') @spec.stubs(:valid_publish_config?).returns(true) @spec.stubs(:publish_config_access).returns('restricted') @spec.stubs(:registry).returns("@private:registry=some_private_registry") assert_equal ['assert-npm-version-tag', 'npm publish --tag latest --access restricted'], @spec.deploy_steps end test '#npmrc_contents returns a scoped private package configuration when the package is scoped and private' do registry = "@shopify:registry=some_private_registry" Shipit.stubs(:npm_org_scope).returns('@shopify') Shipit.stubs(:private_npm_registry).returns('some_private_registry') @spec.stubs(:scoped_package?).returns(true) @spec.stubs(:publish_config_access).returns('restricted') assert_equal registry, @spec.registry end test '#npmrc_contents returns a public scoped package configuration when the package is scoped and public' do registry = "@shopify:registry=https://registry.npmjs.org/" Shipit.stubs(:npm_org_scope).returns('@shopify') @spec.stubs(:scoped_package?).returns(true) @spec.stubs(:publish_config_access).returns('public') assert_equal registry, @spec.registry end test '#npmrc_contents returns a public non-scoped package configuration when the package is not scoped and public' do registry = "registry=https://registry.npmjs.org/" @spec.stubs(:scoped_package?).returns(false) @spec.stubs(:publish_config_access).returns('public') assert_equal registry, @spec.registry end test '#publish_lerna_packages guesses npm tag' do @spec.stubs(:lerna?).returns(true) @spec.stubs(:lerna_config).returns('lerna' => '2.0.0', 'version' => '1.0.0-alpha.1') assert_match(/--npm-tag next/, @spec.deploy_steps.last) end test '#publish_npm_package checks if version tag and a pre-release flag exist, and then invokes npm deploy script' do @spec.stubs(:npm?).returns(true) @spec.stubs(:package_version).returns('1.0.0-alpha.1') @spec.stubs(:valid_publish_config?).returns(true) @spec.stubs(:publish_config_access).returns('restricted') @spec.stubs(:registry).returns("@private:registry=some_private_registry") assert_equal ['assert-npm-version-tag', 'npm publish --tag next --access restricted'], @spec.deploy_steps end test 'bundler installs take priority over yarn installs' do @spec.expects(:discover_yarn).never @spec.stubs(:discover_bundler).returns(['fake bundler task']).once assert_equal ['fake bundler task'], @spec.dependencies_steps end test 'Gems deploys take priority over yarn deploys' do @spec.expects(:discover_yarn_package).never @spec.stubs(:discover_gem).returns(['fake gem task']).once assert_equal ['fake gem task'], @spec.deploy_steps end test '#yarn? is false if there is no package.json' do @spec.expects(:package_json).returns(Shipit::Engine.root.join("tmp-#{SecureRandom.hex}")) @spec.expects(:yarn_lock).returns(Shipit::Engine.root.join('Gemfile')) refute @spec.yarn? end test '#yarn? is false if there is no yarn.lock' do @spec.expects(:yarn_lock).returns(Shipit::Engine.root.join("tmp-#{SecureRandom.hex}")) refute @spec.yarn? end test '#yarn? is false if a private package.json and yarn.lock are present' do package_json = Pathname.new('/tmp/fake_package.json') package_json.write('{"private": true}') @spec.expects(:package_json).returns(package_json) @spec.expects(:yarn_lock).returns(Shipit::Engine.root.join('Gemfile')) refute @spec.yarn? end test '#yarn? is true if a public package.json and yarn.lock are present' do package_json = Pathname.new('/tmp/fake_package.json') package_json.write('{"private": false}') yarn_lock = Pathname.new('/tmp/fake_yarn.lock') yarn_lock.write('') @spec.expects(:package_json).returns(package_json) @spec.expects(:yarn_lock).returns(yarn_lock) assert @spec.yarn? end test '#dependencies_steps returns `yarn install` if a `yarn.lock` is present' do @spec.expects(:yarn?).returns(true).at_least_once assert_equal ['yarn install --no-progress'], @spec.dependencies_steps end test '#publish_npm_package checks if version tag exists, and then invokes npm publish script' do @spec.stubs(:yarn?).returns(true).at_least_once @spec.stubs(:package_version).returns('1.0.0') @spec.stubs(:valid_publish_config?).returns(true) @spec.stubs(:publish_config_access).returns('restricted') @spec.stubs(:registry).returns("@private:registry=some_private_registry") assert_equal ['assert-npm-version-tag', 'npm publish --tag latest --access restricted'], @spec.deploy_steps end test '#publish_npm_package checks if version tag exists, generates npmrc, and then invokes npm publish script when enforce_publish_config? is true' do @spec.stubs(:yarn?).returns(true).at_least_once @spec.stubs(:package_version).returns('1.0.0') @spec.stubs(:valid_publish_config?).returns(true) @spec.stubs(:publish_config_access).returns('restricted') @spec.stubs(:enforce_publish_config?).returns(true) @spec.stubs(:registry).returns('fake') generate_npmrc = 'generate-local-npmrc "fake"' npm_publish = 'npm publish --tag latest --access restricted' deploy_steps = ['assert-npm-version-tag', generate_npmrc, npm_publish] assert_equal deploy_steps, @spec.deploy_steps end test 'yarn checklist takes precedence over npm checklist' do @spec.stubs(:yarn?).returns(true).at_least_once assert_match(/yarn version/, @spec.review_checklist[0]) end test "max_divergence_commits defaults to `nil" do @spec.expects(:load_config).returns({}) assert_nil @spec.max_divergence_commits end test "max_divergence_age defaults to `nil` if `merge.max_divergence.age` cannot be parsed" do @spec.expects(:load_config).returns( 'merge' => { 'max_divergence' => { 'age' => 'badbadbad', }, }, ) assert_nil @spec.max_divergence_age end test "serialised deploy specs are normalised" do stack = shipit_stacks(:shipit) assert_equal stack.cached_deploy_spec_before_type_cast, DeploySpec.dump(stack.cached_deploy_spec) end end end