togglecraft 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe ToggleCraft::Cache do
4
+ let(:cache) { described_class.new(ttl: 2) } # 2 seconds for faster tests
5
+
6
+ after do
7
+ cache.destroy
8
+ end
9
+
10
+ describe '#initialize' do
11
+ it 'creates a cache with default memory adapter' do
12
+ cache = described_class.new
13
+ expect(cache.adapter).to be_a(ToggleCraft::CacheAdapters::MemoryAdapter)
14
+ cache.destroy
15
+ end
16
+
17
+ it 'uses default TTL of 300 seconds' do
18
+ cache = described_class.new
19
+ expect(cache.default_ttl).to eq(300)
20
+ cache.destroy
21
+ end
22
+
23
+ it 'accepts custom TTL' do
24
+ cache = described_class.new(ttl: 600)
25
+ expect(cache.default_ttl).to eq(600)
26
+ cache.destroy
27
+ end
28
+
29
+ it 'accepts custom adapter' do
30
+ custom_adapter = ToggleCraft::CacheAdapters::MemoryAdapter.new
31
+ cache = described_class.new(adapter: custom_adapter)
32
+ expect(cache.adapter).to eq(custom_adapter)
33
+ cache.destroy
34
+ end
35
+ end
36
+
37
+ describe '#set and #get' do
38
+ it 'stores and retrieves values' do
39
+ cache.set('key', 'value')
40
+ expect(cache.get('key')).to eq('value')
41
+ end
42
+
43
+ it 'returns nil for non-existent keys' do
44
+ expect(cache.get('missing')).to be_nil
45
+ end
46
+
47
+ it 'stores different types of values' do
48
+ cache.set('string', 'text')
49
+ cache.set('number', 123)
50
+ cache.set('hash', { foo: 'bar' })
51
+ cache.set('array', [1, 2, 3])
52
+ cache.set('boolean', true)
53
+
54
+ expect(cache.get('string')).to eq('text')
55
+ expect(cache.get('number')).to eq(123)
56
+ expect(cache.get('hash')).to eq({ foo: 'bar' })
57
+ expect(cache.get('array')).to eq([1, 2, 3])
58
+ expect(cache.get('boolean')).to be true
59
+ end
60
+ end
61
+
62
+ describe 'TTL and expiration' do
63
+ it 'expires entries after TTL' do
64
+ cache.set('key', 'value')
65
+ expect(cache.get('key')).to eq('value')
66
+
67
+ # Wait for expiration (TTL is 2 seconds in test)
68
+ # Sleep a bit longer to account for timing precision
69
+ sleep 2.5
70
+
71
+ expect(cache.get('key')).to be_nil
72
+ end
73
+
74
+ it 'allows custom TTL per entry' do
75
+ cache.set('short', 'value1', ttl: 1)
76
+ cache.set('long', 'value2', ttl: 10)
77
+
78
+ # Sleep a bit longer to ensure expiration
79
+ sleep 1.5
80
+
81
+ expect(cache.get('short')).to be_nil
82
+ expect(cache.get('long')).to eq('value2')
83
+ end
84
+ end
85
+
86
+ describe '#delete' do
87
+ it 'removes an entry' do
88
+ cache.set('key', 'value')
89
+ expect(cache.delete('key')).to be true
90
+ expect(cache.get('key')).to be_nil
91
+ end
92
+
93
+ it 'returns false for non-existent keys' do
94
+ expect(cache.delete('missing')).to be false
95
+ end
96
+ end
97
+
98
+ describe '#clear' do
99
+ it 'removes all entries' do
100
+ cache.set('key1', 'value1')
101
+ cache.set('key2', 'value2')
102
+ cache.set('key3', 'value3')
103
+
104
+ cache.clear
105
+
106
+ expect(cache.get('key1')).to be_nil
107
+ expect(cache.get('key2')).to be_nil
108
+ expect(cache.get('key3')).to be_nil
109
+ end
110
+ end
111
+
112
+ describe '#has?' do
113
+ it 'returns true for existing non-expired entries' do
114
+ cache.set('key', 'value')
115
+ expect(cache.has?('key')).to be true
116
+ end
117
+
118
+ it 'returns false for non-existent keys' do
119
+ expect(cache.has?('missing')).to be false
120
+ end
121
+
122
+ it 'returns false for expired entries' do
123
+ cache.set('key', 'value', ttl: 1)
124
+ sleep 1.5
125
+ expect(cache.has?('key')).to be false
126
+ end
127
+
128
+ it 'returns false after deletion' do
129
+ cache.set('key', 'value')
130
+ cache.delete('key')
131
+ expect(cache.has?('key')).to be false
132
+ end
133
+ end
134
+
135
+ describe '#clear_by_prefix' do
136
+ it 'deletes all keys matching prefix' do
137
+ cache.set('user:123', 'data1')
138
+ cache.set('user:456', 'data2')
139
+ cache.set('session:789', 'data3')
140
+
141
+ cache.clear_by_prefix('user:')
142
+
143
+ expect(cache.get('user:123')).to be_nil
144
+ expect(cache.get('user:456')).to be_nil
145
+ expect(cache.get('session:789')).to eq('data3')
146
+ end
147
+
148
+ it 'does not delete keys not matching prefix' do
149
+ cache.set('flag:test', 'data1')
150
+ cache.set('eval:test', 'data2')
151
+
152
+ cache.clear_by_prefix('flag:')
153
+
154
+ expect(cache.get('flag:test')).to be_nil
155
+ expect(cache.get('eval:test')).to eq('data2')
156
+ end
157
+ end
158
+
159
+ describe '#all_flags' do
160
+ it 'returns all cached flags' do
161
+ cache.set('flag:feature1', { enabled: true })
162
+ cache.set('flag:feature2', { enabled: false })
163
+ cache.set('other:key', 'not a flag')
164
+
165
+ flags = cache.all_flags
166
+ expect(flags).to eq({
167
+ 'feature1' => { enabled: true },
168
+ 'feature2' => { enabled: false }
169
+ })
170
+ end
171
+
172
+ it 'excludes expired flags' do
173
+ cache.set('flag:feature1', { enabled: true })
174
+ cache.set('flag:feature2', { enabled: false }, ttl: 1)
175
+
176
+ sleep 1.5
177
+
178
+ flags = cache.all_flags
179
+ expect(flags).to eq({
180
+ 'feature1' => { enabled: true }
181
+ })
182
+ end
183
+
184
+ it 'returns empty hash when no flags exist' do
185
+ expect(cache.all_flags).to eq({})
186
+ end
187
+ end
188
+
189
+ describe '#set_all_flags' do
190
+ it 'stores multiple flags at once' do
191
+ flags = {
192
+ 'feature1' => { enabled: true },
193
+ 'feature2' => { enabled: false },
194
+ 'feature3' => { enabled: true }
195
+ }
196
+
197
+ cache.set_all_flags(flags)
198
+
199
+ expect(cache.get('flag:feature1')).to eq({ enabled: true })
200
+ expect(cache.get('flag:feature2')).to eq({ enabled: false })
201
+ expect(cache.get('flag:feature3')).to eq({ enabled: true })
202
+ end
203
+
204
+ it 'accepts custom TTL for all flags' do
205
+ flags = {
206
+ 'feature1' => { enabled: true },
207
+ 'feature2' => { enabled: false }
208
+ }
209
+
210
+ cache.set_all_flags(flags, ttl: 1)
211
+
212
+ sleep 1.5
213
+
214
+ expect(cache.get('flag:feature1')).to be_nil
215
+ expect(cache.get('flag:feature2')).to be_nil
216
+ end
217
+
218
+ it 'overwrites existing flags' do
219
+ cache.set('flag:feature1', { enabled: false })
220
+ cache.set_all_flags({ 'feature1' => { enabled: true } })
221
+
222
+ expect(cache.get('flag:feature1')).to eq({ enabled: true })
223
+ end
224
+ end
225
+
226
+ describe 'automatic cleanup' do
227
+ it 'removes expired entries automatically' do
228
+ cache.set('key1', 'value1', ttl: 1)
229
+ cache.set('key2', 'value2', ttl: 10)
230
+
231
+ # Wait for expiration and cleanup cycle
232
+ sleep 1.5
233
+
234
+ # Trigger cleanup manually for testing
235
+ cache.send(:cleanup_expired)
236
+
237
+ # Adapter should not have the expired key
238
+ expect(cache.adapter.has?('key1')).to be false
239
+ expect(cache.get('key2')).to eq('value2')
240
+ end
241
+ end
242
+
243
+ describe '#destroy' do
244
+ it 'stops cleanup thread and clears cache' do
245
+ cache.set('key', 'value')
246
+ cache.destroy
247
+
248
+ expect(cache.get('key')).to be_nil
249
+ expect(cache.instance_variable_get(:@cleanup_thread)).to be_nil
250
+ end
251
+
252
+ it 'is safe to call multiple times' do
253
+ expect { cache.destroy }.not_to raise_error
254
+ expect { cache.destroy }.not_to raise_error
255
+ end
256
+ end
257
+
258
+ describe 'concurrent access' do
259
+ it 'handles concurrent reads and writes safely' do
260
+ threads = 10.times.map do |i|
261
+ Thread.new do
262
+ 10.times do |j|
263
+ key = "thread_#{i}_flag_#{j}"
264
+ value = { enabled: (j % 2).zero? }
265
+ cache.set(key, value)
266
+ expect(cache.get(key)).to eq(value)
267
+ end
268
+ end
269
+ end
270
+
271
+ threads.each(&:join)
272
+ end
273
+ end
274
+ end