Skip to content

Commit d139064

Browse files
author
Jay OConnor
committed
allow cache to be an ActiveSupport::Cache object
1 parent e1502e9 commit d139064

File tree

5 files changed

+143
-109
lines changed

5 files changed

+143
-109
lines changed

lib/firebase_id_token.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'firebase_id_token/exceptions/no_certificates_error'
1010
require 'firebase_id_token/exceptions/certificates_request_error'
1111
require 'firebase_id_token/exceptions/certificates_ttl_error'
12+
require 'firebase_id_token/exceptions/unsupported_cache_operation_error'
1213
require 'firebase_id_token/exceptions/certificate_not_found'
1314
require 'firebase_id_token/configuration'
1415
require 'firebase_id_token/certificates'

lib/firebase_id_token/certificates.rb

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ module FirebaseIdToken
3030
# FirebaseIdToken::Certificates.request! # Downloads certificates.
3131
#
3232
class Certificates
33-
# A Redis instance.
34-
attr_reader :redis
33+
# A common cache store
34+
attr_reader :cache_store
3535
# Certificates saved in the Redis (JSON `String` or `nil`).
3636
attr_reader :local_certs
3737

@@ -146,16 +146,20 @@ def self.find!(kid)
146146
# time, use it to know when to request again.
147147
# @return [Fixnum]
148148
def self.ttl
149-
ttl = new.redis.ttl('certificates')
150-
ttl < 0 ? 0 : ttl
149+
if new.cache_store.respond_to?(:redis)
150+
ttl = new.cache_store.redis.ttl('certificates')
151+
return ttl < 0 ? 0 : ttl
152+
end
153+
# Not all cache stores supported by ActiveSupport::Cache support a TTL
154+
# read on an entrye
155+
raise Exceptions::UnsupportedCacheOperationError.new
151156
end
152157

153158
# Sets two instance attributes: `:redis` and `:local_certs`. Those are
154159
# respectively a Redis instance from {FirebaseIdToken::Configuration} and
155160
# the certificates in it.
156161
def initialize
157-
@redis = Redis::Namespace.new('firebase_id_token',
158-
redis: FirebaseIdToken.configuration.redis)
162+
@cache_store = FirebaseIdToken.configuration.cache_store
159163
@local_certs = read_certificates
160164
end
161165

@@ -178,12 +182,12 @@ def request!
178182
private
179183

180184
def read_certificates
181-
certs = @redis.get 'certificates'
185+
certs = @cache_store.read 'certificates'
182186
certs ? JSON.parse(certs) : {}
183187
end
184188

185189
def save_certificates
186-
@redis.setex 'certificates', ttl, @request.body
190+
@cache_store.write 'certificates', @request.body, expires_in: ttl
187191
@local_certs = read_certificates
188192
end
189193

lib/firebase_id_token/configuration.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ module FirebaseIdToken
44
LIB_PATH = File.expand_path('../../', __FILE__)
55

66
class Configuration
7-
attr_accessor :redis, :project_ids, :certificates
7+
attr_accessor :redis, :project_ids, :certificates, :cache_store
88

99
def initialize
1010
@project_ids = []
1111
@certificates = FirebaseIdToken::Certificates
12+
# support older API where redis attribute was explictly set
13+
if redis
14+
@cache_store = ActiveSupport::Cache.RedisStore.new(redis)
15+
end
1216
end
1317
end
1418
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module FirebaseIdToken
2+
module Exceptions
3+
# @see FirebaseIdToken::Certificates.request
4+
# @see FirebaseIdToken::Certificates.request!
5+
class UnsupportedCacheOperationError < StandardError
6+
def initialize(message = "Cache store does not support a TTL read on entries")
7+
super message
8+
end
9+
end
10+
end
11+
end

spec/firebase_id_token/certificates_spec.rb

Lines changed: 114 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
module FirebaseIdToken
44
describe Certificates do
5-
let (:redis) { Redis::Namespace.new 'firebase_id_token', redis: Redis.new }
65
let (:certs) { File.read('spec/fixtures/files/certificates.json') }
76
let (:cache) { 'public, max-age=19302, must-revalidate, no-transform' }
87
let (:low_cache) { 'public, max-age=2160, must-revalidate, no-transform' }
@@ -22,143 +21,158 @@ module FirebaseIdToken
2221
with(an_instance_of(String)) { response }
2322
}
2423

25-
before :each do
26-
redis.del 'certificates'
27-
mock_request
28-
end
29-
30-
describe '#request' do
31-
it 'requests certificates when Redis database is empty' do
32-
expect(HTTParty).to receive(:get).
33-
with(FirebaseIdToken::Certificates::URL)
34-
described_class.request
24+
shared_examples_for 'a certificate store' do |cache_store, cache_store_supports_ttl|
25+
before :each do
26+
allow(FirebaseIdToken.configuration).to receive(:cache_store).and_return(cache_store)
27+
FirebaseIdToken.configuration.cache_store.delete 'certificates'
28+
mock_request
3529
end
3630

37-
it 'does not requests certificates when Redis database is written' do
38-
expect(HTTParty).to receive(:get).
39-
with(FirebaseIdToken::Certificates::URL).once
40-
2.times { described_class.request }
41-
end
42-
end
43-
44-
describe '#request!' do
45-
it 'always requests certificates' do
46-
expect(HTTParty).to receive(:get).
47-
with(FirebaseIdToken::Certificates::URL).twice
48-
2.times { described_class.request! }
49-
end
31+
describe '#request' do
32+
it 'requests certificates when Redis database is empty' do
33+
expect(HTTParty).to receive(:get).
34+
with(FirebaseIdToken::Certificates::URL)
35+
described_class.request
36+
end
5037

51-
it 'sets the certificate expiration time as Redis TTL' do
52-
described_class.request!
53-
expect(redis.ttl('certificates')).to be > 3600
38+
it 'does not requests certificates when Redis database is written' do
39+
expect(HTTParty).to receive(:get).
40+
with(FirebaseIdToken::Certificates::URL).once
41+
2.times { described_class.request }
42+
end
5443
end
5544

56-
it 'raises a error when certificates expires in less than 1 hour' do
57-
allow(response).to receive(:headers) {{'cache-control' => low_cache}}
58-
expect{ described_class.request! }.
59-
to raise_error(Exceptions::CertificatesTtlError)
60-
end
45+
describe '#request!' do
46+
it 'always requests certificates' do
47+
expect(HTTParty).to receive(:get).
48+
with(FirebaseIdToken::Certificates::URL).twice
49+
2.times { described_class.request! }
50+
end
6151

62-
it 'raises a error when HTTP response code is other than 200' do
63-
allow(response).to receive(:code) { 401 }
64-
expect{ described_class.request! }.
65-
to raise_error(Exceptions::CertificatesRequestError)
66-
end
67-
end
52+
it 'sets the certificate expiration time as Redis TTL', skip: !cache_store_supports_ttl do
53+
described_class.request!
54+
expect(described_class.ttl).to be > 3600
55+
end
6856

69-
describe '#request_anyway' do
70-
it 'also requests certificates' do
71-
expect(HTTParty).to receive(:get).
72-
with(FirebaseIdToken::Certificates::URL)
57+
it 'raises a error when certificates expires in less than 1 hour' do
58+
allow(response).to receive(:headers) {{'cache-control' => low_cache}}
59+
expect{ described_class.request! }.
60+
to raise_error(Exceptions::CertificatesTtlError)
61+
end
7362

74-
described_class.request_anyway
63+
it 'raises a error when HTTP response code is other than 200' do
64+
allow(response).to receive(:code) { 401 }
65+
expect{ described_class.request! }.
66+
to raise_error(Exceptions::CertificatesRequestError)
67+
end
7568
end
76-
end
7769

78-
describe '.present?' do
79-
it 'returns false when Redis database is empty' do
80-
expect(described_class.present?).to be(false)
81-
end
70+
describe '#request_anyway' do
71+
it 'also requests certificates' do
72+
expect(HTTParty).to receive(:get).
73+
with(FirebaseIdToken::Certificates::URL)
8274

83-
it 'returns true when Redis database is written' do
84-
described_class.request
85-
expect(described_class.present?).to be(true)
86-
end
87-
end
88-
89-
describe '.all' do
90-
context 'before requesting certificates' do
91-
it 'returns a empty Array' do
92-
expect(described_class.all).to eq([])
75+
described_class.request_anyway
9376
end
9477
end
9578

96-
context 'after requesting certificates' do
97-
it 'returns a array of hashes: String keys' do
98-
described_class.request
99-
expect(described_class.all.first.keys[0]).to be_a(String)
79+
describe '.present?' do
80+
it 'returns false when Redis database is empty' do
81+
expect(described_class.present?).to be(false)
10082
end
10183

102-
it 'returns a array of hashes: OpenSSL::X509::Certificate values' do
84+
it 'returns true when Redis database is written' do
10385
described_class.request
104-
expect(described_class.all.first.values[0]).
105-
to be_a(OpenSSL::X509::Certificate)
86+
expect(described_class.present?).to be(true)
10687
end
10788
end
108-
end
10989

110-
describe '.find' do
111-
context 'without certificates in Redis database' do
112-
it 'raises a exception' do
113-
expect{ described_class.find(kid)}.
114-
to raise_error(Exceptions::NoCertificatesError)
90+
describe '.all' do
91+
context 'before requesting certificates' do
92+
it 'returns a empty Array' do
93+
expect(described_class.all).to eq([])
94+
end
95+
end
96+
97+
context 'after requesting certificates' do
98+
it 'returns a array of hashes: String keys' do
99+
described_class.request
100+
expect(described_class.all.first.keys[0]).to be_a(String)
101+
end
102+
103+
it 'returns a array of hashes: OpenSSL::X509::Certificate values' do
104+
described_class.request
105+
expect(described_class.all.first.values[0]).
106+
to be_a(OpenSSL::X509::Certificate)
107+
end
115108
end
116109
end
117110

118-
context 'with certificates in Redis database' do
119-
it 'returns a OpenSSL::X509::Certificate when it finds the kid' do
120-
described_class.request
121-
expect(described_class.find(kid)).to be_a(OpenSSL::X509::Certificate)
111+
describe '.find' do
112+
context 'without certificates in Redis database' do
113+
it 'raises a exception' do
114+
expect{ described_class.find(kid)}.
115+
to raise_error(Exceptions::NoCertificatesError)
116+
end
122117
end
123118

124-
it 'returns nil when it can not find the kid' do
125-
described_class.request
126-
expect(described_class.find('')).to be(nil)
119+
context 'with certificates in Redis database' do
120+
it 'returns a OpenSSL::X509::Certificate when it finds the kid' do
121+
described_class.request
122+
expect(described_class.find(kid)).to be_a(OpenSSL::X509::Certificate)
123+
end
124+
125+
it 'returns nil when it can not find the kid' do
126+
described_class.request
127+
expect(described_class.find('')).to be(nil)
128+
end
127129
end
128130
end
129-
end
130131

131-
describe '.find!' do
132-
context 'without certificates in Redis database' do
133-
it 'raises a exception' do
134-
expect{ described_class.find!(kid)}.
135-
to raise_error(Exceptions::NoCertificatesError)
132+
describe '.find!' do
133+
context 'without certificates in Redis database' do
134+
it 'raises a exception' do
135+
expect{ described_class.find!(kid)}.
136+
to raise_error(Exceptions::NoCertificatesError)
137+
end
138+
end
139+
context 'with certificates in Redis database' do
140+
it 'returns a OpenSSL::X509::Certificate when it finds the kid' do
141+
described_class.request
142+
expect(described_class.find!(kid)).to be_a(OpenSSL::X509::Certificate)
143+
end
144+
145+
it 'raises a CertificateNotFound error when it can not find the kid' do
146+
described_class.request
147+
expect { described_class.find!('') }
148+
.to raise_error(Exceptions::CertificateNotFound, /Unable to find/)
149+
end
136150
end
151+
137152
end
138-
context 'with certificates in Redis database' do
139-
it 'returns a OpenSSL::X509::Certificate when it finds the kid' do
153+
154+
describe '.ttl' do
155+
it 'returns a positive number when has certificates in Redis', skip: !cache_store_supports_ttl do
140156
described_class.request
141-
expect(described_class.find!(kid)).to be_a(OpenSSL::X509::Certificate)
157+
expect(described_class.ttl).to be > 0
142158
end
143159

144-
it 'raises a CertificateNotFound error when it can not find the kid' do
145-
described_class.request
146-
expect { described_class.find!('') }
147-
.to raise_error(Exceptions::CertificateNotFound, /Unable to find/)
160+
it 'returns zero when has no certificates in Redis', skip: !cache_store_supports_ttl do
161+
expect(described_class.ttl).to eq(0)
148162
end
149-
end
150163

164+
it 'raises an error if the cache store does not support TTL', skip: cache_store_supports_ttl do
165+
expect { described_class.ttl }.to raise_error(Exceptions::UnsupportedCacheOperationError)
166+
end
167+
end
151168
end
152169

153-
describe '.ttl' do
154-
it 'returns a positive number when has certificates in Redis' do
155-
described_class.request
156-
expect(described_class.ttl).to be > 0
157-
end
170+
context 'RedisCacheStore' do
171+
it_behaves_like 'a certificate store', ActiveSupport::Cache::RedisCacheStore.new, true
172+
end
158173

159-
it 'returns zero when has no certificates in Redis' do
160-
expect(described_class.ttl).to eq(0)
161-
end
174+
context 'MemoryStore' do
175+
it_behaves_like 'a certificate store', ActiveSupport::Cache::MemoryStore.new, false
162176
end
163177
end
164178
end

0 commit comments

Comments
 (0)