diff --git a/docs/reference.md b/docs/reference.md index 4a78518..20697be 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -319,6 +319,7 @@ Public error classes: - `FixtureKit::InvalidFixtureDeclaration` - `FixtureKit::MultipleFixtures` - `FixtureKit::CacheMissingError` +- `FixtureKit::CacheCorruptError` - `FixtureKit::FixtureDefinitionNotFound` - `FixtureKit::RunnerAlreadyStartedError` - `FixtureKit::CircularFixtureInheritance` diff --git a/lib/fixture_kit.rb b/lib/fixture_kit.rb index 7caf094..f84cd14 100644 --- a/lib/fixture_kit.rb +++ b/lib/fixture_kit.rb @@ -6,6 +6,14 @@ class DuplicateNameError < Error; end class InvalidFixtureDeclaration < Error; end class MultipleFixtures < Error; end class CacheMissingError < Error; end + class CacheCorruptError < Error + def self.for(path, cause) + new( + "FixtureKit cache file at #{path} is corrupt or malformed " \ + "(#{cause.class}: #{cause.message}). Delete it and re-run to regenerate." + ) + end + end class FixtureDefinitionNotFound < Error; end class RunnerAlreadyStartedError < Error; end class CircularFixtureInheritance < Error; end diff --git a/lib/fixture_kit/file_cache.rb b/lib/fixture_kit/file_cache.rb index 51da248..8730760 100644 --- a/lib/fixture_kit/file_cache.rb +++ b/lib/fixture_kit/file_cache.rb @@ -17,7 +17,7 @@ def exists? end def read - content = JSON.parse(File.read(path)) + content = parse data = content.fetch("data").to_h do |coder_name, coder_data| coder = coder_for(coder_name) @@ -60,6 +60,19 @@ def serialize_exposed(exposed) private + # Reads and parses the cache file, validating that the required top-level + # keys are present. The rescue is scoped to just this step so that decode + # errors raised later in #read (e.g. an unregistered coder, a configuration + # error) are not misreported as a corrupt cache file. + def parse + content = JSON.parse(File.read(path)) + content.fetch("data") + content.fetch("exposed") + content + rescue JSON::ParserError, KeyError => e + raise FixtureKit::CacheCorruptError.for(path, e) + end + def coder_for(class_name) @coder_for ||= FixtureKit.runner.coders.index_by { |c| c.class.name } @coder_for.fetch(class_name) diff --git a/spec/unit/file_cache_spec.rb b/spec/unit/file_cache_spec.rb index e3415e8..0342548 100644 --- a/spec/unit/file_cache_spec.rb +++ b/spec/unit/file_cache_spec.rb @@ -83,6 +83,24 @@ expect(File.exist?(nested_path)).to be(true) end + + it "raises CacheCorruptError when the file contains invalid JSON" do + FileUtils.mkdir_p(cache_path) + File.write(file_path, "this is not json") + + expect { file_cache.read }.to raise_error(FixtureKit::CacheCorruptError) do |error| + expect(error.message).to include(file_path) + expect(error.message).to include("JSON::ParserError") + end + end + + it "raises CacheCorruptError when the JSON is missing required keys" do + FileUtils.mkdir_p(cache_path) + File.write(file_path, JSON.dump({ "data" => {} })) # no "exposed" key + + expect { file_cache.read } + .to raise_error(FixtureKit::CacheCorruptError, /is corrupt or malformed.*KeyError/) + end end describe "#serialize_exposed" do