|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require_relative 'spec_helper' |
| 4 | + |
| 5 | +module RedisProxySpec |
| 6 | + class FakeRedis |
| 7 | + class Error < StandardError; end |
| 8 | + |
| 9 | + attr_reader :calls |
| 10 | + |
| 11 | + def initialize(raises: nil) |
| 12 | + @raises = raises |
| 13 | + @calls = [] |
| 14 | + end |
| 15 | + |
| 16 | + def get(key) |
| 17 | + record(:get, key) |
| 18 | + end |
| 19 | + |
| 20 | + def set(key, value) |
| 21 | + record(:set, [key, value]) |
| 22 | + end |
| 23 | + |
| 24 | + def setex(key, expires_in, value) |
| 25 | + record(:setex, [key, expires_in, value]) |
| 26 | + end |
| 27 | + |
| 28 | + def del(*keys) |
| 29 | + record(:del, keys) |
| 30 | + end |
| 31 | + |
| 32 | + def pipelined |
| 33 | + record(:pipelined) |
| 34 | + yield(self) if block_given? |
| 35 | + [1] |
| 36 | + end |
| 37 | + |
| 38 | + def incrby(key, amount) |
| 39 | + record(:incrby, [key, amount]) |
| 40 | + end |
| 41 | + |
| 42 | + def expire(key, ttl) |
| 43 | + record(:expire, [key, ttl]) |
| 44 | + end |
| 45 | + |
| 46 | + def scan(_cursor, **_opts) |
| 47 | + ["0", []] |
| 48 | + end |
| 49 | + |
| 50 | + def record(method, args = nil) |
| 51 | + @calls << [method, args] |
| 52 | + raise @raises if @raises |
| 53 | + |
| 54 | + :ok |
| 55 | + end |
| 56 | + end |
| 57 | +end |
| 58 | + |
| 59 | +describe Rack::Attack::StoreProxy::RedisProxy do |
| 60 | + before do |
| 61 | + skip 'redis gem not available' unless defined?(::Redis) |
| 62 | + end |
| 63 | + |
| 64 | + it "declares its default as ['Redis::BaseConnectionError']" do |
| 65 | + _(Rack::Attack::StoreProxy::RedisProxy.default_bypassable_store_errors) |
| 66 | + .must_equal ['Redis::BaseConnectionError'] |
| 67 | + end |
| 68 | + |
| 69 | + it "bypasses Redis::BaseConnectionError by default on #read" do |
| 70 | + redis = RedisProxySpec::FakeRedis.new(raises: Redis::BaseConnectionError.new("down")) |
| 71 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new(redis) |
| 72 | + _(proxy.read("k")).must_be_nil |
| 73 | + end |
| 74 | + |
| 75 | + it "bypasses Redis::BaseConnectionError subclasses on #write" do |
| 76 | + redis = RedisProxySpec::FakeRedis.new(raises: Redis::CannotConnectError.new("down")) |
| 77 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new(redis) |
| 78 | + _(proxy.write("k", "v")).must_be_nil |
| 79 | + end |
| 80 | + |
| 81 | + it "re-raises other errors by default" do |
| 82 | + redis = RedisProxySpec::FakeRedis.new(raises: RuntimeError.new("boom")) |
| 83 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new(redis) |
| 84 | + _(-> { proxy.read("k") }).must_raise RuntimeError |
| 85 | + end |
| 86 | + |
| 87 | + it "bypasses custom errors when configured" do |
| 88 | + redis = RedisProxySpec::FakeRedis.new(raises: RedisProxySpec::FakeRedis::Error.new("oom")) |
| 89 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new( |
| 90 | + redis, |
| 91 | + bypassable_store_errors: [RedisProxySpec::FakeRedis::Error] |
| 92 | + ) |
| 93 | + _(proxy.read("k")).must_be_nil |
| 94 | + end |
| 95 | + |
| 96 | + it "bypasses all errors when configured with :all" do |
| 97 | + redis = RedisProxySpec::FakeRedis.new(raises: RuntimeError.new("anything")) |
| 98 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new(redis, bypassable_store_errors: :all) |
| 99 | + _(proxy.read("k")).must_be_nil |
| 100 | + end |
| 101 | + |
| 102 | + it "re-raises default errors when configured with :none" do |
| 103 | + redis = RedisProxySpec::FakeRedis.new(raises: Redis::BaseConnectionError.new("down")) |
| 104 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new(redis, bypassable_store_errors: :none) |
| 105 | + _(-> { proxy.read("k") }).must_raise Redis::BaseConnectionError |
| 106 | + end |
| 107 | + |
| 108 | + it "user-provided Array overrides the default (does not merge)" do |
| 109 | + redis = RedisProxySpec::FakeRedis.new(raises: Redis::BaseConnectionError.new("down")) |
| 110 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new( |
| 111 | + redis, |
| 112 | + bypassable_store_errors: [RedisProxySpec::FakeRedis::Error] |
| 113 | + ) |
| 114 | + _(-> { proxy.read("k") }).must_raise Redis::BaseConnectionError |
| 115 | + end |
| 116 | + |
| 117 | + it "returns nil from #increment on a matching error" do |
| 118 | + redis = RedisProxySpec::FakeRedis.new(raises: Redis::BaseConnectionError.new("down")) |
| 119 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new(redis) |
| 120 | + _(proxy.increment("k", 1, expires_in: 60)).must_be_nil |
| 121 | + end |
| 122 | + |
| 123 | + it "returns nil from #delete on a matching error" do |
| 124 | + redis = RedisProxySpec::FakeRedis.new(raises: Redis::BaseConnectionError.new("down")) |
| 125 | + proxy = Rack::Attack::StoreProxy::RedisProxy.new(redis) |
| 126 | + _(proxy.delete("k")).must_be_nil |
| 127 | + end |
| 128 | + |
| 129 | + it "raises MisconfiguredStoreError for invalid config" do |
| 130 | + redis = RedisProxySpec::FakeRedis.new |
| 131 | + _(-> { |
| 132 | + Rack::Attack::StoreProxy::RedisProxy.new(redis, bypassable_store_errors: :sometimes) |
| 133 | + }).must_raise Rack::Attack::MisconfiguredStoreError |
| 134 | + end |
| 135 | +end |
0 commit comments