From c1e75bca552218e0d5a1ab7228237ebc67ef34cd Mon Sep 17 00:00:00 2001
From: Malcolm Locke <mlocke@gitlab.com>
Date: Mon, 19 Feb 2024 14:48:23 +0000
Subject: [PATCH] Stub exist? in FileReadHelpers

It's a reasonable expectation that if `File.read` should work for
a stubbed filename then `File.exist?` should return true for the
same value.
---
 .../testing_guide/best_practices.md           | 19 ++++---
 spec/support/helpers/file_read_helpers.rb     | 54 ++++++++++---------
 2 files changed, 41 insertions(+), 32 deletions(-)

diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index f5af911e802ca..d355b613a1c53 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -1194,26 +1194,29 @@ a single test triggers the rate limit, the `:disable_rate_limit` can be used ins
 
 In the situations where you need to
 [stub](https://rspec.info/features/3-12/rspec-mocks/basics/allowing-messages/)
-methods such as `File.read`, make sure to:
+the contents of a file use the `stub_file_read`, and
+`expect_file_read` helper methods which handle the stubbing for
+`File.read` correctly. These methods stub `File.read` for the given
+filename, and also stub `File.exist?` to return `true`.
 
-1. Stub `File.read` for only the file path you are interested in.
-1. Call the original implementation for other file paths.
+If you need to manually stub `File.read` for any reason be sure to:
+
+1. Stub and call the original implementation for other file paths.
+1. Then stub `File.read` for only the file path you are interested in.
 
 Otherwise `File.read` calls from other parts of the codebase get
-stubbed incorrectly. You should use the `stub_file_read`, and
-`expect_file_read` helper methods which does the stubbing for
-`File.read` correctly.
+stubbed incorrectly.
 
 ```ruby
 # bad, all Files will read and return nothing
 allow(File).to receive(:read)
 
 # good
-stub_file_read(my_filepath)
+stub_file_read(my_filepath, content: "fake file content")
 
 # also OK
 allow(File).to receive(:read).and_call_original
-allow(File).to receive(:read).with(my_filepath)
+allow(File).to receive(:read).with(my_filepath).and_return("fake file_content")
 ```
 
 #### File system
diff --git a/spec/support/helpers/file_read_helpers.rb b/spec/support/helpers/file_read_helpers.rb
index c30a9e6466f92..9e5517415a409 100644
--- a/spec/support/helpers/file_read_helpers.rb
+++ b/spec/support/helpers/file_read_helpers.rb
@@ -1,24 +1,37 @@
 # frozen_string_literal: true
 
 module FileReadHelpers
-  def stub_file_read(file, content: nil, error: nil)
-    allow_original_file_read
+  def stub_file_read(file, content: nil, error: nil, exist: true)
+    allow_or_expect_file_read(
+      file, mode: :allow, content: content, error: error, exist: exist
+    )
+  end
 
-    expectation = allow(File).to receive(:read).with(file)
+  def expect_file_read(file, content: nil, error: nil, exist: true)
+    allow_or_expect_file_read(
+      file, mode: :expect, content: content, error: error, exist: exist
+    )
+  end
 
-    if error
-      expectation.and_raise(error)
-    elsif content
-      expectation.and_return(content)
-    else
-      expectation
-    end
+  def expect_file_not_to_read(file)
+    allow_original_file_calls
+
+    expect(File).not_to receive(:read).with(file)
   end
 
-  def expect_file_read(file, content: nil, error: nil)
-    allow_original_file_read
+  private
+
+  def allow_or_expect_file_read(file, mode:, content:, error:, exist:)
+    allow_original_file_calls
+    allow(File).to receive(:exist?).with(file).and_return(exist)
 
-    expectation = expect(File).to receive(:read).with(file)
+    expectation = if mode == :allow
+                    allow(File).to receive(:read).with(file)
+                  elsif mode == :expect
+                    expect(File).to receive(:read).with(file)
+                  else
+                    raise ArgumentError, "expected :allow or :expect for `mode`, got `#{mode}`"
+                  end
 
     if error
       expectation.and_raise(error)
@@ -29,20 +42,13 @@ def expect_file_read(file, content: nil, error: nil)
     end
   end
 
-  def expect_file_not_to_read(file)
-    allow_original_file_read
-
-    expect(File).not_to receive(:read).with(file)
-  end
-
-  private
-
-  def allow_original_file_read
+  def allow_original_file_calls
     # Don't set this mock twice, otherwise subsequent calls will clobber
     # previous mocks
-    return if @allow_original_file_read
+    return if @allow_original_file_calls
 
-    @allow_original_file_read = true
+    @allow_original_file_calls = true
     allow(File).to receive(:read).and_call_original
+    allow(File).to receive(:exist?).and_call_original
   end
 end
-- 
GitLab