From d2ebdc84c689dae74b70f8e97eb4d8071cbc77f3 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sat, 27 Jun 2026 23:09:25 -0700 Subject: [PATCH] Support matchers and regexps for the source property (#980) `matches_parameter?` special-cases the `source` property as `Array(expected) == Array(value)` because Chef may store source as an Array. But wrapping the expectation in an Array and comparing with `==` means an RSpec matcher or Regexp expectation can never match: e.g. `.with(source: end_with("MyFile.pem"))` became `[] == ["C:/MyFile.pem"]`, which is always false. Try `expected === value` first so matchers, Regexps, and exact values all work against a scalar source, then keep the array-normalized comparison as a fallback so a plain string still matches a single-element array source (the original behavior). Signed-off-by: Tim Smith --- lib/chefspec/matchers/resource_matcher.rb | 8 +++-- spec/unit/matchers/resource_matcher_spec.rb | 35 ++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/chefspec/matchers/resource_matcher.rb b/lib/chefspec/matchers/resource_matcher.rb index 25b188b9..d1402435 100644 --- a/lib/chefspec/matchers/resource_matcher.rb +++ b/lib/chefspec/matchers/resource_matcher.rb @@ -115,8 +115,12 @@ def unmatched_parameters def matches_parameter?(parameter, expected) value = safe_send(parameter) if parameter == :source - # Chef 11+ stores the source parameter internally as an Array - Array(expected) == Array(value) + # Chef may store the source parameter internally as an Array. Match the + # expectation against the value directly first, so RSpec matchers and + # Regexps work against a scalar source, then fall back to comparing the + # array-normalized forms so a plain string still matches a single-element + # array source. + expected === value || Array(expected) == Array(value) elsif expected.is_a?(Class) # Ruby can't compare classes with === expected == value diff --git a/spec/unit/matchers/resource_matcher_spec.rb b/spec/unit/matchers/resource_matcher_spec.rb index aec15441..9db99cb5 100644 --- a/spec/unit/matchers/resource_matcher_spec.rb +++ b/spec/unit/matchers/resource_matcher_spec.rb @@ -1,5 +1,38 @@ require "spec_helper" describe ChefSpec::Matchers::ResourceMatcher do - pending + subject { described_class.new(:windows_certificate, :create, "CN=example.com") } + + describe "matching the :source parameter" do + # `matches_parameter?` is the private comparison that `.with(source: ...)` + # relies on. Drive it directly with a stubbed resource value. + def source_matches?(expected, actual_source) + allow(subject).to receive(:resource).and_return(double(source: actual_source)) + subject.send(:matches_parameter?, :source, expected) + end + + it "matches an exact string source" do + expect(source_matches?("C:/MyFile.pem", "C:/MyFile.pem")).to be(true) + end + + it "matches a plain string against an array-valued source" do + expect(source_matches?("foo.erb", ["foo.erb"])).to be(true) + end + + it "matches an exact array source" do + expect(source_matches?(%w{a b}, %w{a b})).to be(true) + end + + it "matches an RSpec matcher against a scalar source (issue #980)" do + expect(source_matches?(end_with("MyFile.pem"), "C:/MyFile.pem")).to be(true) + end + + it "matches a Regexp against a scalar source" do + expect(source_matches?(/MyFile\.pem/, "C:/MyFile.pem")).to be(true) + end + + it "does not match when the matcher does not apply" do + expect(source_matches?(end_with("other.pem"), "C:/MyFile.pem")).to be(false) + end + end end