mirror of
https://github.com/Homebrew/brew.git
synced 2026-10-28 09:34:42 +08:00
- Match implicit trust to the documented installation behaviour. Other commands require existing trust for the tap or item. - Retain the dispatched name in `Homebrew.running_command` and limit `ARGV` allowances to `install`, `reinstall` and `upgrade`, which announce and persist item trust. - Preserve early formula loading for command-specific options before `trust_fully_qualified_items!` runs. - Cover formula and cask loads, both `--tap` forms and existing alias, rename and external command behaviour.
998 lines
38 KiB
Ruby
998 lines
38 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
require "cask/cask_loader"
|
|
require "formulary"
|
|
require "tap"
|
|
require "trust"
|
|
|
|
RSpec.describe Homebrew::Trust, :trust_store do
|
|
it "lets HOMEBREW_NO_REQUIRE_TAP_TRUST override HOMEBREW_REQUIRE_TAP_TRUST" do
|
|
allow(Homebrew::EnvConfig).to receive(:odeprecated)
|
|
with_env(HOMEBREW_REQUIRE_TAP_TRUST: "1", HOMEBREW_NO_REQUIRE_TAP_TRUST: "1") do
|
|
expect(Homebrew::EnvConfig.require_tap_trust?).to be(false)
|
|
end
|
|
end
|
|
|
|
it "trusts third-party taps" do
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
|
|
expect(described_class.trusted_tap?(tap)).to be(false)
|
|
|
|
described_class.trust!(:tap, "thirdparty/foo")
|
|
|
|
expect(described_class.trusted_tap?(tap)).to be(true)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not trust a custom-remote tap by its name but does by its remote URL" do
|
|
tap = Tap.fetch("thirdparty", "custom")
|
|
tap.path.mkpath
|
|
system "git", "-C", tap.path.to_s, "init"
|
|
system "git", "-C", tap.path.to_s, "remote", "add", "origin", "https://gitlab.com/other/repo"
|
|
|
|
described_class.trust!(:tap, "thirdparty/custom")
|
|
expect(described_class.trusted_tap?(tap)).to be(false)
|
|
|
|
described_class.trust!(:tap, "https://gitlab.com/other/repo")
|
|
expect(described_class.trusted_tap?(tap)).to be(true)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "trusts a custom-remote tap passed as a Tap object" do
|
|
tap = Tap.fetch("thirdparty", "custom")
|
|
tap.path.mkpath
|
|
system "git", "-C", tap.path.to_s, "init"
|
|
system "git", "-C", tap.path.to_s, "remote", "add", "origin", "https://gitlab.com/other/repo"
|
|
|
|
described_class.trust!(:tap, tap)
|
|
expect(described_class.trusted_tap?(tap)).to be(true)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "rejects a Tap object for a non-tap trust type" do
|
|
tap = Tap.fetch("thirdparty", "custom")
|
|
expect { described_class.trust!(:formula, tap) }.to raise_error(ArgumentError)
|
|
end
|
|
|
|
it "canonicalises a GitHub default-remote URL to the tap name" do
|
|
result = described_class.target("https://github.com/thirdparty/homebrew-foo", type: :tap)
|
|
expect(result).to eq([:tap, "thirdparty/foo"])
|
|
end
|
|
|
|
it "stores a non-GitHub URL verbatim" do
|
|
result = described_class.target("https://gitlab.com/other/repo", type: :tap)
|
|
expect(result).to eq([:tap, "https://gitlab.com/other/repo"])
|
|
end
|
|
|
|
it "trusts a not-yet-installed tap by its non-GitHub remote URL" do
|
|
described_class.trust!(:tap, "https://gitlab.com/absent/repo")
|
|
expect(described_class.trusted_entries(:tap)).to include("https://gitlab.com/absent/repo")
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
end
|
|
|
|
it "untrusts a tap by its remote URL" do
|
|
described_class.trust!(:tap, "https://gitlab.com/other/repo")
|
|
type, trust_name = described_class.target("https://gitlab.com/other/repo", type: :tap, include_existing: true)
|
|
removed = described_class.untrust!(type, trust_name)
|
|
expect(removed).to be(true)
|
|
expect(described_class.trusted_entries(:tap)).not_to include("https://gitlab.com/other/repo")
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
end
|
|
|
|
it "invalidates old tap trust entries after a redirect" do
|
|
described_class.trust!(:tap, "thirdparty/foo")
|
|
described_class.trust!(:tap, "https://gitlab.com/old/repo")
|
|
described_class.trust!(:formula, "thirdparty/foo/bar")
|
|
described_class.trust!(:cask, "thirdparty/foo/baz")
|
|
described_class.trust!(:command, "thirdparty/foo/hello")
|
|
|
|
expect(described_class.invalidate_tap_references!("thirdparty/foo",
|
|
remote: "https://gitlab.com/old/repo")).to be(true)
|
|
|
|
expect(described_class.trusted_entries(:tap)).to be_empty
|
|
expect(described_class.trusted_entries(:formula)).to be_empty
|
|
expect(described_class.trusted_entries(:cask)).to be_empty
|
|
expect(described_class.trusted_entries(:command)).to be_empty
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
described_class.clear!(:formula)
|
|
described_class.clear!(:cask)
|
|
described_class.clear!(:command)
|
|
end
|
|
|
|
it "infers tap type for a remote URL argument" do
|
|
result = described_class.target("https://gitlab.com/other/repo")
|
|
expect(result).to eq([:tap, "https://gitlab.com/other/repo"])
|
|
end
|
|
|
|
it "infers tap type for an scp-style remote URL argument" do
|
|
result = described_class.target("git@gitlab.com:other/repo")
|
|
expect(result).to eq([:tap, "git@gitlab.com:other/repo"])
|
|
end
|
|
|
|
it "rejects a bare @-string rather than trusting it as a tap" do
|
|
expect { described_class.target("foo@bar") }
|
|
.to raise_error(UsageError, /fully-qualified/)
|
|
expect(described_class.trusted_entries(:tap)).to be_empty
|
|
end
|
|
|
|
it "rejects a bare @-string even with an explicit tap type" do
|
|
expect { described_class.target("not@valid", type: :tap) }
|
|
.to raise_error(UsageError, /Invalid tap name/)
|
|
expect(described_class.trusted_entries(:tap)).to be_empty
|
|
end
|
|
|
|
it "trusts custom-remote tap items by remote but still resolves existing entries to untrust" do
|
|
tap = Tap.fetch("thirdparty", "custom")
|
|
tap.path.mkpath
|
|
system "git", "-C", tap.path.to_s, "init"
|
|
system "git", "-C", tap.path.to_s, "remote", "add", "origin", "https://gitlab.com/other/repo"
|
|
|
|
described_class.trust!(*described_class.target("thirdparty/custom/bar", type: :formula))
|
|
|
|
expect(described_class.trusted?(:formula, "thirdparty/custom/bar")).to be(true)
|
|
expect(described_class.trusted_entries(:formula)).to contain_exactly("https://gitlab.com/other/repo/bar")
|
|
|
|
described_class.trust!(:formula, "thirdparty/custom/legacy")
|
|
expect(described_class.target("thirdparty/custom/legacy", type: :formula, include_existing: true))
|
|
.to eq([:formula, "thirdparty/custom/legacy"])
|
|
ensure
|
|
described_class.clear!(:formula)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "keys an item by a declared custom remote before the tap is installed" do
|
|
result = described_class.target("thirdparty/custom/bar", type: :formula,
|
|
tap_remote: "https://gitlab.com/other/repo")
|
|
expect(result).to eq([:formula, "https://gitlab.com/other/repo/bar"])
|
|
end
|
|
|
|
it "keys an item by its tap name when the declared remote is its default remote" do
|
|
result = described_class.target("thirdparty/custom/bar", type: :formula,
|
|
tap_remote: "https://github.com/thirdparty/homebrew-custom")
|
|
expect(result).to eq([:formula, "thirdparty/custom/bar"])
|
|
end
|
|
|
|
it "keeps a declared remote custom relative to its tap name even when it resembles another default" do
|
|
result = described_class.target("thirdparty/custom/bar", type: :formula,
|
|
tap_remote: "https://github.com/other/homebrew-project")
|
|
expect(result).to eq([:formula, "https://github.com/other/homebrew-project/bar"])
|
|
end
|
|
|
|
it "keys a whole tap by a declared custom remote before the tap is installed" do
|
|
result = described_class.target("thirdparty/custom", type: :tap, tap_remote: "https://gitlab.com/other/repo")
|
|
expect(result).to eq([:tap, "https://gitlab.com/other/repo"])
|
|
end
|
|
|
|
it "trusts formulae from trusted taps" do
|
|
Tap.fetch("trustedformulae", "foo")
|
|
|
|
described_class.trust!(:tap, "trustedformulae/foo")
|
|
|
|
expect(described_class.trusted?(:formula, "trustedformulae/foo/bar")).to be(true)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"trustedformulae"
|
|
end
|
|
|
|
it "ignores a trust file with a non-object JSON root" do
|
|
trust_file = T.let(nil, T.nilable(Pathname))
|
|
trust_file = described_class.trust_file
|
|
trust_file.dirname.mkpath
|
|
trust_file.write("[]")
|
|
|
|
expect(described_class.trusted?(:tap, "thirdparty/foo")).to be(false)
|
|
ensure
|
|
trust_file.unlink if trust_file&.exist?
|
|
end
|
|
|
|
it "uses the provided home when the trust file path is home-based" do
|
|
root_home = Pathname(TEST_TMPDIR)/"root-home"
|
|
sudo_home = Pathname(TEST_TMPDIR)/"sudo-home"
|
|
allow(Dir).to receive(:home).with(ENV.fetch("USER")).and_return(root_home.to_s)
|
|
|
|
with_env(HOMEBREW_USER_CONFIG_HOME: root_home/".homebrew") do
|
|
expect(described_class.trust_file(home: sudo_home)).to eq(sudo_home/".homebrew/trust.json")
|
|
end
|
|
end
|
|
|
|
it "keeps the configured trust file path when it is not home-based" do
|
|
root_home = Pathname(TEST_TMPDIR)/"root-home"
|
|
sudo_home = Pathname(TEST_TMPDIR)/"sudo-home"
|
|
config_home = Pathname(TEST_TMPDIR)/"xdg-config/homebrew"
|
|
allow(Dir).to receive(:home).with(ENV.fetch("USER")).and_return(root_home.to_s)
|
|
|
|
with_env(HOMEBREW_USER_CONFIG_HOME: config_home) do
|
|
expect(described_class.trust_file(home: sudo_home)).to eq(config_home/"trust.json")
|
|
end
|
|
end
|
|
|
|
it "falls back to the current home when USER cannot be looked up" do
|
|
home = Pathname(TEST_TMPDIR)/"home"
|
|
|
|
with_env(USER: "nonexistent_user_zzz", HOME: home, HOMEBREW_USER_CONFIG_HOME: home/".homebrew") do
|
|
expect { expect(described_class.trust_file).to eq(home/".homebrew/trust.json") }
|
|
.to output(/Could not determine home directory for `\$USER` \(nonexistent_user_zzz\)/).to_stderr
|
|
end
|
|
end
|
|
|
|
it "trusts a GitHub SSH-remote tap by its name" do
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
tap.path.mkpath
|
|
system "git", "-C", tap.path.to_s, "init"
|
|
system "git", "-C", tap.path.to_s, "remote", "add", "origin", "git@github.com:thirdparty/homebrew-foo"
|
|
# Guard the setup so the test genuinely exercises SSH-vs-HTTPS equivalence: a
|
|
# remote-less tap would also be trusted by name, passing for the wrong reason.
|
|
expect(tap.remote).to eq("git@github.com:thirdparty/homebrew-foo")
|
|
|
|
described_class.trust!(:tap, "thirdparty/foo")
|
|
|
|
expect(described_class.trusted_tap?(tap)).to be(true)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "untrusts third-party taps" do
|
|
described_class.trust!(:tap, "thirdparty/foo")
|
|
|
|
expect(described_class.untrust!(:tap, "thirdparty/foo")).to be(true)
|
|
expect(described_class.trusted?(:tap, "thirdparty/foo")).to be(false)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
end
|
|
|
|
it "does not lose entries when trusting concurrently" do
|
|
names = Array.new(10) { |i| "thirdparty/foo/formula#{i}" }
|
|
|
|
names.map do |name|
|
|
Thread.new { described_class.trust!(:formula, name) }
|
|
end.each(&:join)
|
|
|
|
expect(described_class.trusted_entries(:formula)).to match_array(names)
|
|
ensure
|
|
described_class.clear!(:formula)
|
|
end
|
|
|
|
it "trusts fully-qualified formulae and casks" do
|
|
tap = Tap.fetch("qualified", "foo")
|
|
tap.formula_dir.mkpath
|
|
tap.cask_dir.mkpath
|
|
(tap.formula_dir/"bar.rb").write("class Bar < Formula; end\n")
|
|
(tap.cask_dir/"baz.rb").write("cask 'baz'\n")
|
|
|
|
without_partial_double_verification do
|
|
expect($stderr).to receive(:ohai).with("Trusted formula qualified/foo/bar").ordered
|
|
expect($stderr).to receive(:ohai).with("Trusted cask qualified/foo/baz").ordered
|
|
|
|
described_class.trust_fully_qualified_items!(["qualified/foo/bar", "qualified/foo/baz"])
|
|
end
|
|
|
|
expect(described_class.trusted?(:formula, "qualified/foo/bar")).to be(true)
|
|
expect(described_class.trusted?(:cask, "qualified/foo/baz")).to be(true)
|
|
ensure
|
|
described_class.clear!(:formula)
|
|
described_class.clear!(:cask)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"qualified"
|
|
end
|
|
|
|
it "does not trust missing fully-qualified formulae or casks" do
|
|
Tap.fetch("thirdparty", "foo")
|
|
|
|
described_class.trust_fully_qualified_items!(["thirdparty/foo/bar"], type: :formula)
|
|
described_class.trust_fully_qualified_items!(["thirdparty/foo/baz"], type: :cask)
|
|
|
|
expect(described_class.trusted?(:formula, "thirdparty/foo/bar")).to be(false)
|
|
expect(described_class.trusted?(:cask, "thirdparty/foo/baz")).to be(false)
|
|
ensure
|
|
described_class.clear!(:formula)
|
|
described_class.clear!(:cask)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
shared_examples "a fully qualified formula alias or rename" do |name|
|
|
T.bind(self, T.class_of(RSpec::Core::ExampleGroup))
|
|
|
|
sig { returns(Tap) }
|
|
let(:tap) { Tap.fetch("thirdparty", "foo") }
|
|
|
|
sig { returns(Pathname) }
|
|
let(:formula_path) { tap.formula_dir/"bar.rb" }
|
|
|
|
sig { returns(String) }
|
|
let(:full_name) { "#{tap.name}/#{name}" }
|
|
|
|
around do |example|
|
|
old_argv = ARGV.dup
|
|
ARGV.clear
|
|
example.run
|
|
ensure
|
|
ARGV.replace(old_argv)
|
|
end
|
|
|
|
before do
|
|
allow(Homebrew).to receive(:running_command).and_return("install")
|
|
without_partial_double_verification { allow($stderr).to receive(:ohai) }
|
|
formula_path.dirname.mkpath
|
|
formula_path.write <<~RUBY
|
|
class Bar < Formula
|
|
url "https://brew.sh/bar-1.0.tar.gz"
|
|
end
|
|
RUBY
|
|
tap.alias_dir.mkpath
|
|
(tap.alias_dir/"23bar").make_relative_symlink(formula_path)
|
|
(tap.path/"formula_renames.json").write(JSON.generate({ "oldbar" => "bar" }))
|
|
end
|
|
|
|
after do
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "refuses to load an untrusted formula through an alias or rename" do
|
|
ARGV.replace([full_name])
|
|
|
|
expect { Formulary.factory(full_name, warn: false) }
|
|
.to raise_error(Homebrew::UntrustedTapError, %r{brew trust --formula thirdparty/foo/bar})
|
|
end
|
|
|
|
it "does not allow a formula file through an alias or rename" do
|
|
ARGV.replace([full_name])
|
|
|
|
expect(described_class.trusted_formula_file?(formula_path)).to be(false)
|
|
end
|
|
|
|
it "requires the canonical name when implicitly trusting a formula" do
|
|
formula_path.write("raise 'must not evaluate formula to trust it'\n")
|
|
|
|
expect { described_class.trust_fully_qualified_items!([full_name]) }
|
|
.to raise_error(UsageError, %r{brew trust --formula thirdparty/foo/bar})
|
|
end
|
|
|
|
it "requires the canonical name when inferring a trust target" do
|
|
expect { described_class.target(full_name) }
|
|
.to raise_error(UsageError, %r{brew trust --formula thirdparty/foo/bar})
|
|
end
|
|
|
|
it "requires the canonical name when the formula type is specified" do
|
|
formula_path.write("raise 'must not evaluate formula to trust it'\n")
|
|
|
|
expect { described_class.trust_fully_qualified_items!([full_name], type: :formula) }
|
|
.to raise_error(UsageError, %r{brew trust --formula thirdparty/foo/bar})
|
|
end
|
|
|
|
it "requires the canonical name for an explicit formula trust target" do
|
|
expect { described_class.target(full_name, type: :formula) }
|
|
.to raise_error(UsageError, %r{brew trust --formula thirdparty/foo/bar})
|
|
end
|
|
|
|
it "does not write trust entries when refusing an alias or rename" do
|
|
expect do
|
|
described_class.trust_fully_qualified_items!([full_name])
|
|
rescue UsageError
|
|
nil
|
|
end.not_to change { described_class.trust_file.exist? }
|
|
end
|
|
|
|
it "loads an alias or rename after trusting its canonical name" do
|
|
described_class.trust!(*described_class.target("#{tap.name}/bar", type: :formula))
|
|
ARGV.replace([full_name])
|
|
|
|
expect(Formulary.factory(full_name, warn: false).full_name).to eq("#{tap.name}/bar")
|
|
end
|
|
|
|
it "does not grant additional trust when installing an already trusted target" do
|
|
described_class.trust!(:formula, "#{tap.name}/bar")
|
|
|
|
expect { described_class.trust_fully_qualified_items!([full_name]) }
|
|
.not_to change { described_class.trust_file.read }
|
|
end
|
|
|
|
it "prefers aliases and renames to formula filenames" do
|
|
(tap.formula_dir/"#{name}.rb").write("raise 'must not trust the shadowed formula'\n")
|
|
|
|
expect { described_class.trust_fully_qualified_items!([full_name]) }
|
|
.to raise_error(UsageError, %r{brew trust --formula thirdparty/foo/bar})
|
|
end
|
|
|
|
it "does not authorise the shadowed formula file" do
|
|
(tap.formula_dir/"#{name}.rb").write("raise 'must not load the shadowed formula'\n")
|
|
ARGV.replace([full_name])
|
|
|
|
expect { described_class.require_trusted_formula!(name, tap.formula_dir/"#{name}.rb") }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
end
|
|
|
|
it "requires the canonical name for a custom remote" do
|
|
allow(tap).to receive(:remote).and_return("https://gitlab.com/other/repo")
|
|
|
|
expect { described_class.target(full_name, type: :formula) }
|
|
.to raise_error(UsageError, %r{brew trust --formula thirdparty/foo/bar})
|
|
end
|
|
|
|
it "resolves an alias or rename when revoking canonical trust" do
|
|
described_class.trust!(:formula, "#{tap.name}/bar")
|
|
|
|
expect(described_class.target(full_name, type: :formula, include_existing: true))
|
|
.to eq([:formula, "#{tap.name}/bar"])
|
|
end
|
|
|
|
it "preserves existing literal entries for revocation" do
|
|
described_class.trust!(:formula, full_name)
|
|
|
|
expect(described_class.target(full_name, type: :formula, include_existing: true))
|
|
.to eq([:formula, full_name])
|
|
end
|
|
|
|
it "does not allow the unqualified name" do
|
|
ARGV.replace([name])
|
|
|
|
expect { described_class.require_trusted_formula!("bar", formula_path) }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
end
|
|
|
|
it "does not allow unrelated formulae" do
|
|
ARGV.replace([full_name])
|
|
|
|
expect { described_class.require_trusted_formula!("other", tap.formula_dir/"other.rb") }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
end
|
|
|
|
it "does not allow casks with the resolved formula name" do
|
|
ARGV.replace([full_name])
|
|
|
|
expect { described_class.require_trusted_cask!("bar", tap.cask_dir/"bar.rb") }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
end
|
|
|
|
it "does not allow a formula symlinked into another tap" do
|
|
target_path = Tap.fetch("thirdparty", "other").formula_dir/"bar.rb"
|
|
target_path.dirname.mkpath
|
|
FileUtils.mv formula_path, target_path
|
|
formula_path.make_relative_symlink(target_path)
|
|
ARGV.replace([full_name])
|
|
|
|
expect { Formulary.factory(full_name, warn: false) }
|
|
.to raise_error(Homebrew::UntrustedTapError, %r{thirdparty/other/bar})
|
|
end
|
|
|
|
it "does not trust a missing formula" do
|
|
formula_path.unlink
|
|
|
|
described_class.trust_fully_qualified_items!([full_name])
|
|
|
|
expect(described_class.trusted_entries(:formula)).to be_empty
|
|
end
|
|
|
|
it "requires new canonical trust when the explicitly named target changes" do
|
|
described_class.trust!(*described_class.target("#{tap.name}/bar", type: :formula))
|
|
(tap.formula_dir/"other.rb").write("raise 'must not evaluate the new target'\n")
|
|
(tap.alias_dir/"23bar").unlink
|
|
(tap.alias_dir/"23bar").make_relative_symlink(tap.formula_dir/"other.rb")
|
|
(tap.path/"formula_renames.json").write(JSON.generate({ "oldbar" => "other" }))
|
|
tap.clear_cache
|
|
ARGV.replace([full_name])
|
|
|
|
expect { Formulary.factory(full_name, warn: false) }
|
|
.to raise_error(Homebrew::UntrustedTapError, %r{thirdparty/foo/other})
|
|
end
|
|
|
|
it "does not use another remote's metadata for a declared remote" do
|
|
allow(tap).to receive(:remote).and_return("https://gitlab.com/other/repo")
|
|
|
|
expect(described_class.target(full_name, type: :formula, tap_remote: "https://gitlab.com/different/repo"))
|
|
.to eq([:formula, "https://gitlab.com/different/repo/#{name}"])
|
|
end
|
|
|
|
it "requires the canonical name for a matching declared remote" do
|
|
allow(tap).to receive(:remote).and_return("https://gitlab.com/other/repo")
|
|
|
|
expect { described_class.target(full_name, type: :formula, tap_remote: "https://gitlab.com/other/repo") }
|
|
.to raise_error(UsageError, %r{brew trust --formula thirdparty/foo/bar})
|
|
end
|
|
end
|
|
|
|
it_behaves_like "a fully qualified formula alias or rename", "23bar"
|
|
it_behaves_like "a fully qualified formula alias or rename", "oldbar"
|
|
|
|
it "does not follow cross-tap migrations" do
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
tap.path.mkpath
|
|
(tap.path/"tap_migrations.json").write(JSON.generate({ "migrated" => "thirdparty/other/bar" }))
|
|
target_path = Tap.fetch("thirdparty", "other").formula_dir/"bar.rb"
|
|
target_path.dirname.mkpath
|
|
target_path.write("raise 'must not evaluate migrated formula'\n")
|
|
old_argv = ARGV.dup
|
|
ARGV.replace(["#{tap.name}/migrated"])
|
|
described_class.trust_fully_qualified_items!(ARGV)
|
|
|
|
expect { Formulary.factory(ARGV.fetch(0), warn: false) }
|
|
.to raise_error(Homebrew::UntrustedTapError, %r{thirdparty/other/bar})
|
|
ensure
|
|
ARGV.replace(old_argv) if old_argv
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not report taps with trusted entries as wholly untrusted" do
|
|
allow(described_class).to receive(:untrusted_taps)
|
|
.and_return([
|
|
instance_double(Tap, name: "thirdparty/foo", reference: "thirdparty/foo", uses_custom_remote?: false),
|
|
])
|
|
described_class.trust!(:formula, "thirdparty/foo/bar")
|
|
|
|
expect(described_class.wholly_untrusted_taps).to be_empty
|
|
ensure
|
|
described_class.clear!(:formula)
|
|
end
|
|
|
|
it "writes the trust store with user-only permissions" do
|
|
described_class.trust!(:tap, "thirdparty/foo")
|
|
|
|
trust_file = described_class.trust_file
|
|
expect(trust_file.stat.mode & 0777).to eq(0600)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
end
|
|
|
|
it "creates the trust store directory with user-only permissions" do
|
|
trust_file = described_class.trust_file
|
|
FileUtils.rm_rf trust_file.dirname
|
|
old_umask = T.let(nil, T.nilable(Integer))
|
|
old_umask = File.umask(0002)
|
|
|
|
described_class.trust!(:tap, "thirdparty/foo")
|
|
|
|
expect(trust_file.dirname.stat.mode & 0777).to eq(0700)
|
|
ensure
|
|
File.umask(old_umask) if old_umask
|
|
described_class.clear!(:tap)
|
|
end
|
|
|
|
it "rejects a trust store in a group-writable directory" do
|
|
trust_file = T.let(nil, T.nilable(Pathname))
|
|
trust_file = described_class.trust_file
|
|
trust_file.dirname.chmod(0770)
|
|
|
|
expect { described_class.trust!(:tap, "thirdparty/foo") }
|
|
.to raise_error(Homebrew::InsecureTrustStoreError, /Refusing to write insecure trust store/)
|
|
ensure
|
|
if trust_file
|
|
trust_file.dirname.chmod(0700) if trust_file.dirname.exist?
|
|
FileUtils.rm_f trust_file
|
|
end
|
|
end
|
|
|
|
it "rejects a group-writable trust store" do
|
|
trust_file = T.let(nil, T.nilable(Pathname))
|
|
trust_file = described_class.trust_file
|
|
trust_file.write(JSON.generate({ trustedtaps: ["thirdparty/foo"] }))
|
|
trust_file.chmod(0660)
|
|
|
|
expect { described_class.trust!(:tap, "thirdparty/bar") }
|
|
.to raise_error(Homebrew::InsecureTrustStoreError, /Refusing to write insecure trust store/)
|
|
ensure
|
|
trust_file.chmod(0600) if trust_file&.exist?
|
|
FileUtils.rm_f trust_file if trust_file
|
|
end
|
|
|
|
it "writes a symlinked trust store through to its target" do
|
|
trust_file = described_class.trust_file
|
|
target_dir = T.let(nil, T.nilable(Pathname))
|
|
target_dir = Pathname(TEST_TMPDIR)/"trust-target"
|
|
target_dir.mkpath
|
|
target_file = target_dir/"trust.json"
|
|
target_file.write(JSON.generate({ trustedtaps: ["thirdparty/foo"] }))
|
|
FileUtils.ln_s target_file, trust_file
|
|
|
|
described_class.trust!(:tap, "thirdparty/bar")
|
|
|
|
expect(trust_file).to be_a_symlink
|
|
expect(JSON.parse(target_file.read).fetch("trustedtaps")).to eq(["thirdparty/bar", "thirdparty/foo"])
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf target_dir if target_dir
|
|
end
|
|
|
|
it "rejects a symlinked trust store in a group-writable directory" do
|
|
trust_file = described_class.trust_file
|
|
target_dir = T.let(nil, T.nilable(Pathname))
|
|
target_dir = Pathname(TEST_TMPDIR)/"trust-target"
|
|
target_dir.mkpath
|
|
target_dir.chmod(0770)
|
|
FileUtils.ln_s target_dir/"trust.json", trust_file
|
|
|
|
expect { described_class.trust!(:tap, "thirdparty/foo") }
|
|
.to raise_error(Homebrew::InsecureTrustStoreError, /Refusing to write insecure trust store/)
|
|
ensure
|
|
target_dir.chmod(0700) if target_dir&.exist?
|
|
FileUtils.rm_rf target_dir if target_dir
|
|
end
|
|
|
|
it "rejects a symlinked trust store pointing to another symlink" do
|
|
trust_file = described_class.trust_file
|
|
target_dir = T.let(nil, T.nilable(Pathname))
|
|
target_dir = Pathname(TEST_TMPDIR)/"trust-target"
|
|
target_dir.mkpath
|
|
FileUtils.ln_s target_dir/"real-trust.json", target_dir/"trust.json"
|
|
FileUtils.ln_s target_dir/"trust.json", trust_file
|
|
|
|
expect { described_class.trust!(:tap, "thirdparty/foo") }
|
|
.to raise_error(Homebrew::InsecureTrustStoreError, /Refusing to write insecure trust store/)
|
|
ensure
|
|
FileUtils.rm_rf target_dir if target_dir
|
|
end
|
|
|
|
it "requires third-party taps by default" do
|
|
described_class.clear!(:tap)
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
|
|
expect { described_class.require_trusted_formula!("default-trust", formula_path) }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
|
|
expect(described_class.trusted?(:tap, "thirdparty/foo")).to be(false)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "requires trust for a symlink outside the tap directory that resolves into an untrusted tap" do
|
|
tap = Tap.fetch("thirdparty", "linked-source")
|
|
formula_path = tap.formula_dir/"linked-formula.rb"
|
|
formula_path.dirname.mkpath
|
|
formula_path.write("class LinkedFormula < Formula; end\n")
|
|
linked_path = Pathname(TEST_TMPDIR)/"linked-formula.rb"
|
|
FileUtils.ln_s formula_path, linked_path
|
|
|
|
expect { described_class.require_trusted_formula!("linked-formula", linked_path) }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
ensure
|
|
FileUtils.rm_f linked_path if linked_path
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "uses the canonical tap identity when a symlink crosses between taps" do
|
|
trusted_tap = Tap.fetch("thirdparty", "trusted-links")
|
|
untrusted_tap = Tap.fetch("thirdparty", "untrusted-targets")
|
|
target_path = untrusted_tap.formula_dir/"linked-formula.rb"
|
|
target_path.dirname.mkpath
|
|
target_path.write("class LinkedFormula < Formula; end\n")
|
|
linked_path = trusted_tap.formula_dir/"linked-formula.rb"
|
|
linked_path.dirname.mkpath
|
|
FileUtils.ln_s target_path, linked_path
|
|
described_class.trust!(:tap, trusted_tap)
|
|
|
|
expect { described_class.require_trusted_formula!("linked-formula", linked_path) }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "preserves the nominal tap identity for a symlinked whole-tap root" do
|
|
tap = Tap.fetch("thirdparty", "linked-root")
|
|
real_tap_path = mktmpdir/"linked-root"
|
|
(real_tap_path/"Formula").mkpath
|
|
tap.path.parent.mkpath
|
|
FileUtils.ln_s real_tap_path, tap.path
|
|
formula_path = tap.formula_dir/"linked-formula.rb"
|
|
formula_path.write("class LinkedFormula < Formula; end\n")
|
|
described_class.trust!(:tap, tap)
|
|
|
|
expect { described_class.require_trusted_formula!("linked-formula", formula_path) }.not_to raise_error
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "falls back to the nominal tap identity for a symlink loop" do
|
|
tap = Tap.fetch("thirdparty", "looping-link")
|
|
formula_path = tap.formula_dir/"looping-formula.rb"
|
|
formula_path.dirname.mkpath
|
|
FileUtils.ln_s formula_path.basename, formula_path
|
|
|
|
expect { described_class.require_trusted_formula!("looping-formula", formula_path) }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
ensure
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "reads the invoking user's trust store for sudoed services" do
|
|
root_home = Pathname(TEST_TMPDIR)/"root-home"
|
|
sudo_home = Pathname(TEST_TMPDIR)/"sudo-home"
|
|
(sudo_home/".homebrew").mkpath
|
|
(sudo_home/".homebrew/trust.json").write(JSON.generate({ trustedtaps: ["thirdparty/foo"] }))
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
|
|
allow(Homebrew).to receive(:running_as_root?).and_return(true)
|
|
allow(Dir).to receive(:home).with(ENV.fetch("USER")).and_return(root_home.to_s)
|
|
allow(Etc).to receive(:getpwnam).with("brewuser").and_return(double(dir: sudo_home.to_s))
|
|
|
|
with_env(HOME: root_home, HOMEBREW_SUDO_USER: "brewuser", HOMEBREW_USER_CONFIG_HOME: root_home/".homebrew") do
|
|
expect { described_class.require_trusted_formula!("default-trust", formula_path) }.not_to raise_error
|
|
end
|
|
ensure
|
|
FileUtils.rm_rf root_home if root_home
|
|
FileUtils.rm_rf sudo_home if sudo_home
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "reads the invoking user's XDG trust store under sudo" do
|
|
xdg_config_home = Pathname(TEST_TMPDIR)/"sudo-xdg-config"
|
|
(xdg_config_home/"homebrew").mkpath
|
|
(xdg_config_home/"homebrew/trust.json").write(JSON.generate({ trustedtaps: ["thirdparty/foo"] }))
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
|
|
allow(Homebrew).to receive(:running_as_root?).and_return(true)
|
|
allow(Etc).to receive(:getpwnam).with("brewuser").and_return(double(dir: "/nonexistent"))
|
|
|
|
with_env(HOMEBREW_SUDO_USER: "brewuser",
|
|
HOMEBREW_USER_CONFIG_HOME: xdg_config_home/"homebrew",
|
|
XDG_CONFIG_HOME: xdg_config_home) do
|
|
expect { described_class.require_trusted_formula!("default-trust", formula_path) }.not_to raise_error
|
|
end
|
|
ensure
|
|
FileUtils.rm_rf xdg_config_home if xdg_config_home
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "reads the invoking user's Homebrew XDG trust store under sudo" do
|
|
xdg_config_home = Pathname(TEST_TMPDIR)/"sudo-homebrew-xdg-config"
|
|
(xdg_config_home/"homebrew").mkpath
|
|
(xdg_config_home/"homebrew/trust.json").write(JSON.generate({ trustedtaps: ["thirdparty/foo"] }))
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
|
|
allow(Homebrew).to receive(:running_as_root?).and_return(true)
|
|
allow(Etc).to receive(:getpwnam).with("brewuser").and_return(double(dir: "/nonexistent"))
|
|
|
|
with_env(HOMEBREW_SUDO_USER: "brewuser",
|
|
HOMEBREW_USER_CONFIG_HOME: xdg_config_home/"homebrew",
|
|
XDG_CONFIG_HOME: nil,
|
|
HOMEBREW_XDG_CONFIG_HOME: xdg_config_home) do
|
|
expect { described_class.require_trusted_formula!("default-trust", formula_path) }.not_to raise_error
|
|
end
|
|
ensure
|
|
FileUtils.rm_rf xdg_config_home if xdg_config_home
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "warns when the sudo user cannot be looked up" do
|
|
allow(Homebrew).to receive(:running_as_root?).and_return(true)
|
|
allow(Etc).to receive(:getpwnam).with("brewuser").and_raise(ArgumentError)
|
|
|
|
with_env(HOMEBREW_SUDO_USER: "brewuser") do
|
|
expect { expect(described_class.trusted?(:tap, "thirdparty/foo")).to be(false) }
|
|
.to output(
|
|
Regexp.new(
|
|
"Could not determine home directory for `\\$HOMEBREW_SUDO_USER` " \
|
|
"\\(brewuser\\); falling back to " \
|
|
"#{Regexp.escape(described_class.trust_file.to_s)}\\.",
|
|
),
|
|
).to_stderr
|
|
end
|
|
ensure
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not trust or store default trust when checking files" do
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
|
|
expect { expect(described_class.trusted_formula_file?(formula_path)).to be(false) }
|
|
.not_to output.to_stderr
|
|
|
|
expect(described_class.trusted?(:tap, "thirdparty/foo")).to be(false)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not trust untrusted files when trust checks are enabled" do
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
|
|
expect(described_class.trusted_formula_file?(formula_path)).to be(false)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
context "when an untrusted formula path contains a newline" do
|
|
sig { returns(Pathname) }
|
|
let(:formula_path) { Tap.fetch("thirdparty", "foo").formula_dir/"d\ne/default-trust.rb" }
|
|
|
|
before do
|
|
formula_path.dirname.mkpath
|
|
formula_path.write <<~RUBY
|
|
class DefaultTrust < Formula
|
|
url "https://brew.sh/default-trust-1.0.tar.gz"
|
|
end
|
|
RUBY
|
|
end
|
|
|
|
after do
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not trust the formula file" do
|
|
expect(described_class.trusted_formula_file?(formula_path)).to be(false)
|
|
end
|
|
|
|
it "refuses to evaluate the formula" do
|
|
expect { Formulary.factory(formula_path) }.to raise_error(Homebrew::UntrustedTapError)
|
|
end
|
|
end
|
|
|
|
shared_examples "explicit formula installation" do |command|
|
|
T.bind(self, T.class_of(RSpec::Core::ExampleGroup))
|
|
|
|
it "allows explicitly named formula files for #{command}" do
|
|
old_argv = ARGV.dup
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
allow(Homebrew).to receive(:running_command).and_return(command)
|
|
|
|
ARGV.replace(["thirdparty/foo/default-trust"])
|
|
expect(described_class.trusted_formula_file?(formula_path)).to be(true)
|
|
ensure
|
|
ARGV.replace(old_argv) if old_argv
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
end
|
|
|
|
it_behaves_like "explicit formula installation", "install"
|
|
it_behaves_like "explicit formula installation", "reinstall"
|
|
it_behaves_like "explicit formula installation", "upgrade"
|
|
|
|
shared_examples "untrusted tap arguments" do |arguments|
|
|
T.bind(self, T.class_of(RSpec::Core::ExampleGroup))
|
|
|
|
sig { returns(Pathname) }
|
|
let(:formula_path) { Tap.fetch("thirdparty", "foo").formula_dir/"default-trust.rb" }
|
|
|
|
sig { returns(Pathname) }
|
|
let(:cask_path) { Tap.fetch("thirdparty", "foo").cask_dir/"default-trust.rb" }
|
|
|
|
around do |example|
|
|
old_argv = ARGV.dup
|
|
ARGV.replace(arguments)
|
|
example.run
|
|
ensure
|
|
ARGV.replace(old_argv)
|
|
end
|
|
|
|
before do
|
|
allow(Homebrew).to receive(:running_command).and_return("info")
|
|
formula_path.dirname.mkpath
|
|
formula_path.write <<~RUBY
|
|
class DefaultTrust < Formula
|
|
raise "must not evaluate untrusted formula"
|
|
end
|
|
RUBY
|
|
cask_path.dirname.mkpath
|
|
cask_path.write <<~RUBY
|
|
cask "default-trust" do
|
|
raise "must not evaluate untrusted cask"
|
|
end
|
|
RUBY
|
|
end
|
|
|
|
after do
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not trust the formula file" do
|
|
expect(described_class.trusted_formula_file?(formula_path)).to be(false)
|
|
end
|
|
|
|
it "does not trust the cask file" do
|
|
expect(described_class.trusted_cask_file?(cask_path)).to be(false)
|
|
end
|
|
|
|
it "refuses to evaluate the formula" do
|
|
expect { Formulary.factory("thirdparty/foo/default-trust") }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
end
|
|
|
|
it "refuses to evaluate the cask" do
|
|
expect { Cask::CaskLoader.load("thirdparty/foo/default-trust") }
|
|
.to raise_error(Homebrew::UntrustedTapError)
|
|
end
|
|
end
|
|
|
|
it_behaves_like "untrusted tap arguments", ["thirdparty/foo/default-trust"]
|
|
it_behaves_like "untrusted tap arguments", ["--tap=thirdparty/foo"]
|
|
it_behaves_like "untrusted tap arguments", ["--tap", "thirdparty/foo"]
|
|
|
|
it "does not allow files from a tap named as a bare argument when trust checks are enabled" do
|
|
old_argv = ARGV.dup
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
|
|
ARGV.replace(["info", "thirdparty/foo"])
|
|
expect(described_class.trusted_formula_file?(formula_path)).to be(false)
|
|
ensure
|
|
ARGV.replace(old_argv) if old_argv
|
|
described_class.clear!(:tap)
|
|
described_class.clear!(:formula)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not allow explicitly named command files when trust checks are enabled" do
|
|
allow(Homebrew).to receive(:running_command).and_return("install")
|
|
old_argv = ARGV.dup
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
command_path = tap.path/"cmd/brew-default-trust.rb"
|
|
command_path.dirname.mkpath
|
|
|
|
ARGV.replace(["thirdparty/foo/default-trust"])
|
|
expect(described_class.trusted_command_files([command_path])).to eq([])
|
|
ensure
|
|
ARGV.replace(old_argv) if old_argv
|
|
described_class.clear!(:command)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not trust untrusted command files when trust checks are enabled" do
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
command_path = tap.path/"cmd/brew-default-trust.rb"
|
|
command_path.dirname.mkpath
|
|
|
|
expect { expect(described_class.trusted_command_files([command_path])).to eq([]) }
|
|
.to output(%r{Skipping thirdparty/foo because it is not trusted}).to_stderr
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not warn about a partially trusted tap when other files are untrusted" do
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
trusted_path = tap.formula_dir/"trusted.rb"
|
|
untrusted_path = tap.formula_dir/"untrusted.rb"
|
|
trusted_path.dirname.mkpath
|
|
FileUtils.touch [trusted_path, untrusted_path]
|
|
described_class.trust!(:formula, "thirdparty/foo/trusted")
|
|
|
|
expect { expect(described_class.trusted_formula_files([trusted_path, untrusted_path])).to eq([trusted_path]) }
|
|
.not_to output.to_stderr
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
described_class.clear!(:formula)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
|
|
it "does not store default trust when trust checks are disabled" do
|
|
tap = Tap.fetch("thirdparty", "foo")
|
|
formula_path = tap.formula_dir/"default-trust.rb"
|
|
formula_path.dirname.mkpath
|
|
allow(Homebrew::EnvConfig).to receive(:no_require_tap_trust?).and_return(true)
|
|
|
|
expect { described_class.require_trusted_formula!("default-trust", formula_path) }
|
|
.not_to output.to_stderr
|
|
|
|
expect(described_class.trusted?(:tap, "thirdparty/foo")).to be(false)
|
|
ensure
|
|
described_class.clear!(:tap)
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
end
|