nestlens 0.1.1 → 0.2.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.
- package/dist/__tests__/core/storage/memory.storage.spec.d.ts +2 -0
- package/dist/__tests__/core/storage/memory.storage.spec.d.ts.map +1 -0
- package/dist/__tests__/core/storage/memory.storage.spec.js +450 -0
- package/dist/__tests__/core/storage/memory.storage.spec.js.map +1 -0
- package/dist/__tests__/core/storage/redis.storage.spec.d.ts +10 -0
- package/dist/__tests__/core/storage/redis.storage.spec.d.ts.map +1 -0
- package/dist/__tests__/core/storage/redis.storage.spec.js +660 -0
- package/dist/__tests__/core/storage/redis.storage.spec.js.map +1 -0
- package/dist/__tests__/core/storage/storage.factory.spec.d.ts +2 -0
- package/dist/__tests__/core/storage/storage.factory.spec.d.ts.map +1 -0
- package/dist/__tests__/core/storage/storage.factory.spec.js +151 -0
- package/dist/__tests__/core/storage/storage.factory.spec.js.map +1 -0
- package/dist/core/storage/index.d.ts +2 -1
- package/dist/core/storage/index.d.ts.map +1 -1
- package/dist/core/storage/index.js +17 -1
- package/dist/core/storage/index.js.map +1 -1
- package/dist/core/storage/memory.storage.d.ts +59 -0
- package/dist/core/storage/memory.storage.d.ts.map +1 -0
- package/dist/core/storage/memory.storage.js +629 -0
- package/dist/core/storage/memory.storage.js.map +1 -0
- package/dist/core/storage/redis.storage.d.ts +77 -0
- package/dist/core/storage/redis.storage.d.ts.map +1 -0
- package/dist/core/storage/redis.storage.js +595 -0
- package/dist/core/storage/redis.storage.js.map +1 -0
- package/dist/core/storage/storage.factory.d.ts +28 -0
- package/dist/core/storage/storage.factory.d.ts.map +1 -0
- package/dist/core/storage/storage.factory.js +169 -0
- package/dist/core/storage/storage.factory.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/nestlens.config.d.ts +54 -0
- package/dist/nestlens.config.d.ts.map +1 -1
- package/dist/nestlens.config.js +16 -2
- package/dist/nestlens.config.js.map +1 -1
- package/dist/nestlens.module.js +3 -3
- package/dist/nestlens.module.js.map +1 -1
- package/package.json +19 -4
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redis Storage Tests
|
|
4
|
+
*
|
|
5
|
+
* These tests verify the RedisStorage implementation using mocked Redis operations.
|
|
6
|
+
* Since RedisStorage uses dynamic imports, we test the class by:
|
|
7
|
+
* 1. Creating a testable subclass that allows injecting a mock client
|
|
8
|
+
* 2. Testing all public methods with the mock client
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
// Mock pipeline
|
|
12
|
+
const createMockPipeline = () => ({
|
|
13
|
+
hset: jest.fn().mockReturnThis(),
|
|
14
|
+
zadd: jest.fn().mockReturnThis(),
|
|
15
|
+
sadd: jest.fn().mockReturnThis(),
|
|
16
|
+
hgetall: jest.fn().mockReturnThis(),
|
|
17
|
+
hincrby: jest.fn().mockReturnThis(),
|
|
18
|
+
exec: jest.fn().mockResolvedValue([]),
|
|
19
|
+
});
|
|
20
|
+
// Mock Redis client factory
|
|
21
|
+
const createMockRedisClient = () => {
|
|
22
|
+
const pipeline = createMockPipeline();
|
|
23
|
+
return {
|
|
24
|
+
incr: jest.fn(),
|
|
25
|
+
incrby: jest.fn(),
|
|
26
|
+
hset: jest.fn(),
|
|
27
|
+
hget: jest.fn(),
|
|
28
|
+
hgetall: jest.fn(),
|
|
29
|
+
hdel: jest.fn(),
|
|
30
|
+
hincrby: jest.fn(),
|
|
31
|
+
zadd: jest.fn(),
|
|
32
|
+
zcard: jest.fn(),
|
|
33
|
+
zrange: jest.fn(),
|
|
34
|
+
zrevrange: jest.fn(),
|
|
35
|
+
zrangebyscore: jest.fn(),
|
|
36
|
+
zrevrangebyscore: jest.fn(),
|
|
37
|
+
zrem: jest.fn(),
|
|
38
|
+
zcount: jest.fn(),
|
|
39
|
+
sadd: jest.fn(),
|
|
40
|
+
srem: jest.fn(),
|
|
41
|
+
smembers: jest.fn(),
|
|
42
|
+
sinter: jest.fn(),
|
|
43
|
+
sunion: jest.fn(),
|
|
44
|
+
del: jest.fn(),
|
|
45
|
+
keys: jest.fn(),
|
|
46
|
+
quit: jest.fn().mockResolvedValue('OK'),
|
|
47
|
+
pipeline: jest.fn(() => pipeline),
|
|
48
|
+
_pipeline: pipeline, // expose for test assertions
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
// Import the actual class to access its internals for testing
|
|
52
|
+
// We'll use reflection to set the private client
|
|
53
|
+
const redis_storage_1 = require("../../../core/storage/redis.storage");
|
|
54
|
+
/**
|
|
55
|
+
* Helper to create a RedisStorage instance with a mock client
|
|
56
|
+
*/
|
|
57
|
+
async function createTestStorage(config = {}) {
|
|
58
|
+
const storage = new redis_storage_1.RedisStorage({ keyPrefix: 'test:', ...config });
|
|
59
|
+
const mockClient = createMockRedisClient();
|
|
60
|
+
// Use reflection to set the private client
|
|
61
|
+
// This is a test-only pattern to avoid needing ioredis installed
|
|
62
|
+
storage.client = mockClient;
|
|
63
|
+
return { storage, mockClient, mockPipeline: mockClient._pipeline };
|
|
64
|
+
}
|
|
65
|
+
describe('RedisStorage', () => {
|
|
66
|
+
// ==================== Helper Functions ====================
|
|
67
|
+
function createEntry(type, payload = {}) {
|
|
68
|
+
return {
|
|
69
|
+
type,
|
|
70
|
+
payload,
|
|
71
|
+
requestId: `req-${Date.now()}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function createMockEntryHash(id, type, payload = {}) {
|
|
75
|
+
return {
|
|
76
|
+
id: String(id),
|
|
77
|
+
type,
|
|
78
|
+
requestId: `req-${id}`,
|
|
79
|
+
payload: JSON.stringify(payload),
|
|
80
|
+
createdAt: new Date().toISOString(),
|
|
81
|
+
familyHash: '',
|
|
82
|
+
resolvedAt: '',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ==================== Core CRUD Tests ====================
|
|
86
|
+
describe('save', () => {
|
|
87
|
+
it('should save an entry and return it with id and createdAt', async () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const { storage, mockClient } = await createTestStorage();
|
|
90
|
+
mockClient.incr.mockResolvedValue(1);
|
|
91
|
+
const entry = createEntry('request', { method: 'GET', path: '/test' });
|
|
92
|
+
// Act
|
|
93
|
+
const saved = await storage.save(entry);
|
|
94
|
+
// Assert
|
|
95
|
+
expect(saved.id).toBe(1);
|
|
96
|
+
expect(saved.createdAt).toBeDefined();
|
|
97
|
+
expect(saved.type).toBe('request');
|
|
98
|
+
expect(mockClient.hset).toHaveBeenCalled();
|
|
99
|
+
expect(mockClient.zadd).toHaveBeenCalledTimes(2); // all + type index
|
|
100
|
+
await storage.close();
|
|
101
|
+
});
|
|
102
|
+
it('should add entry to request index when requestId is provided', async () => {
|
|
103
|
+
// Arrange
|
|
104
|
+
const { storage, mockClient } = await createTestStorage();
|
|
105
|
+
mockClient.incr.mockResolvedValue(1);
|
|
106
|
+
const entry = createEntry('request', { method: 'GET' });
|
|
107
|
+
entry.requestId = 'req-123';
|
|
108
|
+
// Act
|
|
109
|
+
await storage.save(entry);
|
|
110
|
+
// Assert
|
|
111
|
+
expect(mockClient.sadd).toHaveBeenCalledWith(expect.stringContaining('request:req-123'), '1');
|
|
112
|
+
await storage.close();
|
|
113
|
+
});
|
|
114
|
+
it('should assign incrementing IDs', async () => {
|
|
115
|
+
// Arrange
|
|
116
|
+
const { storage, mockClient } = await createTestStorage();
|
|
117
|
+
mockClient.incr
|
|
118
|
+
.mockResolvedValueOnce(1)
|
|
119
|
+
.mockResolvedValueOnce(2)
|
|
120
|
+
.mockResolvedValueOnce(3);
|
|
121
|
+
// Act
|
|
122
|
+
const saved1 = await storage.save(createEntry('request'));
|
|
123
|
+
const saved2 = await storage.save(createEntry('query'));
|
|
124
|
+
const saved3 = await storage.save(createEntry('log'));
|
|
125
|
+
// Assert
|
|
126
|
+
expect(saved1.id).toBe(1);
|
|
127
|
+
expect(saved2.id).toBe(2);
|
|
128
|
+
expect(saved3.id).toBe(3);
|
|
129
|
+
await storage.close();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('saveBatch', () => {
|
|
133
|
+
it('should save multiple entries at once', async () => {
|
|
134
|
+
// Arrange
|
|
135
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
136
|
+
mockClient.incrby.mockResolvedValue(3);
|
|
137
|
+
const entries = [
|
|
138
|
+
createEntry('request', { path: '/1' }),
|
|
139
|
+
createEntry('request', { path: '/2' }),
|
|
140
|
+
createEntry('request', { path: '/3' }),
|
|
141
|
+
];
|
|
142
|
+
// Act
|
|
143
|
+
const saved = await storage.saveBatch(entries);
|
|
144
|
+
// Assert
|
|
145
|
+
expect(saved).toHaveLength(3);
|
|
146
|
+
expect(saved[0].id).toBe(1);
|
|
147
|
+
expect(saved[1].id).toBe(2);
|
|
148
|
+
expect(saved[2].id).toBe(3);
|
|
149
|
+
expect(mockPipeline.exec).toHaveBeenCalled();
|
|
150
|
+
await storage.close();
|
|
151
|
+
});
|
|
152
|
+
it('should return empty array for empty input', async () => {
|
|
153
|
+
// Arrange
|
|
154
|
+
const { storage } = await createTestStorage();
|
|
155
|
+
// Act
|
|
156
|
+
const saved = await storage.saveBatch([]);
|
|
157
|
+
// Assert
|
|
158
|
+
expect(saved).toEqual([]);
|
|
159
|
+
await storage.close();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('find', () => {
|
|
163
|
+
it('should find entries by type', async () => {
|
|
164
|
+
// Arrange
|
|
165
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
166
|
+
mockClient.zrevrange.mockResolvedValue(['1', '2']);
|
|
167
|
+
mockPipeline.exec.mockResolvedValue([
|
|
168
|
+
[null, createMockEntryHash(1, 'request', { path: '/a' })],
|
|
169
|
+
[null, createMockEntryHash(2, 'request', { path: '/b' })],
|
|
170
|
+
]);
|
|
171
|
+
mockClient.smembers.mockResolvedValue([]); // for tag hydration
|
|
172
|
+
// Act
|
|
173
|
+
const entries = await storage.find({ type: 'request' });
|
|
174
|
+
// Assert
|
|
175
|
+
expect(entries).toHaveLength(2);
|
|
176
|
+
expect(mockClient.zrevrange).toHaveBeenCalledWith(expect.stringContaining('type:request'), 0, 99);
|
|
177
|
+
await storage.close();
|
|
178
|
+
});
|
|
179
|
+
it('should find entries by requestId', async () => {
|
|
180
|
+
// Arrange
|
|
181
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
182
|
+
mockClient.smembers.mockResolvedValueOnce(['1']); // for request lookup
|
|
183
|
+
mockPipeline.exec.mockResolvedValue([
|
|
184
|
+
[null, createMockEntryHash(1, 'request', { path: '/test' })],
|
|
185
|
+
]);
|
|
186
|
+
mockClient.smembers.mockResolvedValue([]); // for tag hydration
|
|
187
|
+
// Act
|
|
188
|
+
const entries = await storage.find({ requestId: 'req-123' });
|
|
189
|
+
// Assert
|
|
190
|
+
expect(entries).toHaveLength(1);
|
|
191
|
+
await storage.close();
|
|
192
|
+
});
|
|
193
|
+
it('should return empty array when no entries found', async () => {
|
|
194
|
+
// Arrange
|
|
195
|
+
const { storage, mockClient } = await createTestStorage();
|
|
196
|
+
mockClient.zrevrange.mockResolvedValue([]);
|
|
197
|
+
// Act
|
|
198
|
+
const entries = await storage.find({ type: 'exception' });
|
|
199
|
+
// Assert
|
|
200
|
+
expect(entries).toEqual([]);
|
|
201
|
+
await storage.close();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('findById', () => {
|
|
205
|
+
it('should find an entry by ID', async () => {
|
|
206
|
+
// Arrange
|
|
207
|
+
const { storage, mockClient } = await createTestStorage();
|
|
208
|
+
mockClient.hgetall.mockResolvedValue(createMockEntryHash(1, 'request', { method: 'GET' }));
|
|
209
|
+
mockClient.smembers.mockResolvedValue([]);
|
|
210
|
+
// Act
|
|
211
|
+
const entry = await storage.findById(1);
|
|
212
|
+
// Assert
|
|
213
|
+
expect(entry).not.toBeNull();
|
|
214
|
+
expect(entry.id).toBe(1);
|
|
215
|
+
expect(entry.type).toBe('request');
|
|
216
|
+
await storage.close();
|
|
217
|
+
});
|
|
218
|
+
it('should return null for non-existent entry', async () => {
|
|
219
|
+
// Arrange
|
|
220
|
+
const { storage, mockClient } = await createTestStorage();
|
|
221
|
+
mockClient.hgetall.mockResolvedValue({});
|
|
222
|
+
// Act
|
|
223
|
+
const entry = await storage.findById(999);
|
|
224
|
+
// Assert
|
|
225
|
+
expect(entry).toBeNull();
|
|
226
|
+
await storage.close();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe('count', () => {
|
|
230
|
+
it('should count all entries', async () => {
|
|
231
|
+
// Arrange
|
|
232
|
+
const { storage, mockClient } = await createTestStorage();
|
|
233
|
+
mockClient.zcard.mockResolvedValue(10);
|
|
234
|
+
// Act
|
|
235
|
+
const count = await storage.count();
|
|
236
|
+
// Assert
|
|
237
|
+
expect(count).toBe(10);
|
|
238
|
+
expect(mockClient.zcard).toHaveBeenCalledWith(expect.stringContaining('entries:all'));
|
|
239
|
+
await storage.close();
|
|
240
|
+
});
|
|
241
|
+
it('should count entries by type', async () => {
|
|
242
|
+
// Arrange
|
|
243
|
+
const { storage, mockClient } = await createTestStorage();
|
|
244
|
+
mockClient.zcard.mockResolvedValue(5);
|
|
245
|
+
// Act
|
|
246
|
+
const count = await storage.count('request');
|
|
247
|
+
// Assert
|
|
248
|
+
expect(count).toBe(5);
|
|
249
|
+
expect(mockClient.zcard).toHaveBeenCalledWith(expect.stringContaining('type:request'));
|
|
250
|
+
await storage.close();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
// ==================== Tag Tests ====================
|
|
254
|
+
describe('addTags', () => {
|
|
255
|
+
it('should add tags to an entry', async () => {
|
|
256
|
+
// Arrange
|
|
257
|
+
const { storage, mockPipeline } = await createTestStorage();
|
|
258
|
+
// Act
|
|
259
|
+
await storage.addTags(1, ['bug', 'urgent']);
|
|
260
|
+
// Assert
|
|
261
|
+
expect(mockPipeline.sadd).toHaveBeenCalledTimes(4); // 2 tags x 2 sets each
|
|
262
|
+
expect(mockPipeline.exec).toHaveBeenCalled();
|
|
263
|
+
await storage.close();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
describe('getEntryTags', () => {
|
|
267
|
+
it('should return tags for an entry', async () => {
|
|
268
|
+
// Arrange
|
|
269
|
+
const { storage, mockClient } = await createTestStorage();
|
|
270
|
+
mockClient.smembers.mockResolvedValue(['bug', 'urgent']);
|
|
271
|
+
// Act
|
|
272
|
+
const tags = await storage.getEntryTags(1);
|
|
273
|
+
// Assert
|
|
274
|
+
expect(tags).toEqual(['bug', 'urgent']);
|
|
275
|
+
await storage.close();
|
|
276
|
+
});
|
|
277
|
+
it('should return empty array for entry with no tags', async () => {
|
|
278
|
+
// Arrange
|
|
279
|
+
const { storage, mockClient } = await createTestStorage();
|
|
280
|
+
mockClient.smembers.mockResolvedValue([]);
|
|
281
|
+
// Act
|
|
282
|
+
const tags = await storage.getEntryTags(1);
|
|
283
|
+
// Assert
|
|
284
|
+
expect(tags).toEqual([]);
|
|
285
|
+
await storage.close();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe('getAllTags', () => {
|
|
289
|
+
it('should return all tags with counts', async () => {
|
|
290
|
+
// Arrange
|
|
291
|
+
const { storage, mockClient } = await createTestStorage();
|
|
292
|
+
mockClient.hgetall.mockResolvedValue({
|
|
293
|
+
bug: '5',
|
|
294
|
+
feature: '3',
|
|
295
|
+
urgent: '2',
|
|
296
|
+
});
|
|
297
|
+
// Act
|
|
298
|
+
const tags = await storage.getAllTags();
|
|
299
|
+
// Assert
|
|
300
|
+
expect(tags).toHaveLength(3);
|
|
301
|
+
expect(tags[0]).toEqual({ tag: 'bug', count: 5 });
|
|
302
|
+
expect(tags[1]).toEqual({ tag: 'feature', count: 3 });
|
|
303
|
+
await storage.close();
|
|
304
|
+
});
|
|
305
|
+
it('should filter out tags with zero count', async () => {
|
|
306
|
+
// Arrange
|
|
307
|
+
const { storage, mockClient } = await createTestStorage();
|
|
308
|
+
mockClient.hgetall.mockResolvedValue({
|
|
309
|
+
bug: '5',
|
|
310
|
+
deleted: '0',
|
|
311
|
+
});
|
|
312
|
+
// Act
|
|
313
|
+
const tags = await storage.getAllTags();
|
|
314
|
+
// Assert
|
|
315
|
+
expect(tags).toHaveLength(1);
|
|
316
|
+
expect(tags[0].tag).toBe('bug');
|
|
317
|
+
await storage.close();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
describe('findByTags', () => {
|
|
321
|
+
it('should find entries with OR logic by default', async () => {
|
|
322
|
+
// Arrange
|
|
323
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
324
|
+
mockClient.sunion.mockResolvedValue(['1', '2']);
|
|
325
|
+
mockPipeline.exec.mockResolvedValue([
|
|
326
|
+
[null, createMockEntryHash(1, 'exception', { name: 'Error' })],
|
|
327
|
+
[null, createMockEntryHash(2, 'exception', { name: 'TypeError' })],
|
|
328
|
+
]);
|
|
329
|
+
mockClient.smembers.mockResolvedValue([]); // for tag hydration
|
|
330
|
+
// Act
|
|
331
|
+
const entries = await storage.findByTags(['bug', 'urgent']);
|
|
332
|
+
// Assert
|
|
333
|
+
expect(mockClient.sunion).toHaveBeenCalled();
|
|
334
|
+
expect(entries).toHaveLength(2);
|
|
335
|
+
await storage.close();
|
|
336
|
+
});
|
|
337
|
+
it('should find entries with AND logic', async () => {
|
|
338
|
+
// Arrange
|
|
339
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
340
|
+
mockClient.sinter.mockResolvedValue(['1']);
|
|
341
|
+
mockPipeline.exec.mockResolvedValue([
|
|
342
|
+
[null, createMockEntryHash(1, 'exception', { name: 'Error' })],
|
|
343
|
+
]);
|
|
344
|
+
mockClient.smembers.mockResolvedValue([]); // for tag hydration
|
|
345
|
+
// Act
|
|
346
|
+
const entries = await storage.findByTags(['bug', 'urgent'], 'AND');
|
|
347
|
+
// Assert
|
|
348
|
+
expect(mockClient.sinter).toHaveBeenCalled();
|
|
349
|
+
expect(entries).toHaveLength(1);
|
|
350
|
+
await storage.close();
|
|
351
|
+
});
|
|
352
|
+
it('should return empty array for no matching tags', async () => {
|
|
353
|
+
// Arrange
|
|
354
|
+
const { storage, mockClient } = await createTestStorage();
|
|
355
|
+
mockClient.sunion.mockResolvedValue([]);
|
|
356
|
+
// Act
|
|
357
|
+
const entries = await storage.findByTags(['nonexistent']);
|
|
358
|
+
// Assert
|
|
359
|
+
expect(entries).toEqual([]);
|
|
360
|
+
await storage.close();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
// ==================== Monitored Tags Tests ====================
|
|
364
|
+
describe('addMonitoredTag', () => {
|
|
365
|
+
it('should add a monitored tag', async () => {
|
|
366
|
+
// Arrange
|
|
367
|
+
const { storage, mockClient } = await createTestStorage();
|
|
368
|
+
mockClient.hget.mockResolvedValue(null);
|
|
369
|
+
mockClient.incr.mockResolvedValue(1);
|
|
370
|
+
// Act
|
|
371
|
+
const monitored = await storage.addMonitoredTag('critical');
|
|
372
|
+
// Assert
|
|
373
|
+
expect(monitored.id).toBe(1);
|
|
374
|
+
expect(monitored.tag).toBe('critical');
|
|
375
|
+
expect(monitored.createdAt).toBeDefined();
|
|
376
|
+
await storage.close();
|
|
377
|
+
});
|
|
378
|
+
it('should return existing monitored tag if already exists', async () => {
|
|
379
|
+
// Arrange
|
|
380
|
+
const { storage, mockClient } = await createTestStorage();
|
|
381
|
+
const existing = { id: 1, tag: 'critical', createdAt: '2024-01-01T00:00:00Z' };
|
|
382
|
+
mockClient.hget.mockResolvedValue(JSON.stringify(existing));
|
|
383
|
+
// Act
|
|
384
|
+
const monitored = await storage.addMonitoredTag('critical');
|
|
385
|
+
// Assert
|
|
386
|
+
expect(monitored).toEqual(existing);
|
|
387
|
+
expect(mockClient.incr).not.toHaveBeenCalled();
|
|
388
|
+
await storage.close();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
describe('getMonitoredTags', () => {
|
|
392
|
+
it('should return all monitored tags', async () => {
|
|
393
|
+
// Arrange
|
|
394
|
+
const { storage, mockClient } = await createTestStorage();
|
|
395
|
+
mockClient.hgetall.mockResolvedValue({
|
|
396
|
+
critical: JSON.stringify({ id: 1, tag: 'critical', createdAt: '2024-01-01T00:00:00Z' }),
|
|
397
|
+
important: JSON.stringify({ id: 2, tag: 'important', createdAt: '2024-01-02T00:00:00Z' }),
|
|
398
|
+
});
|
|
399
|
+
// Act
|
|
400
|
+
const tags = await storage.getMonitoredTags();
|
|
401
|
+
// Assert
|
|
402
|
+
expect(tags).toHaveLength(2);
|
|
403
|
+
expect(tags[0].tag).toBe('critical');
|
|
404
|
+
expect(tags[1].tag).toBe('important');
|
|
405
|
+
await storage.close();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
// ==================== Resolution Tests ====================
|
|
409
|
+
describe('resolveEntry', () => {
|
|
410
|
+
it('should mark an entry as resolved', async () => {
|
|
411
|
+
// Arrange
|
|
412
|
+
const { storage, mockClient } = await createTestStorage();
|
|
413
|
+
// Act
|
|
414
|
+
await storage.resolveEntry(1);
|
|
415
|
+
// Assert
|
|
416
|
+
expect(mockClient.hset).toHaveBeenCalledWith(expect.stringContaining('entries:1'), 'resolvedAt', expect.any(String));
|
|
417
|
+
await storage.close();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
describe('unresolveEntry', () => {
|
|
421
|
+
it('should mark an entry as unresolved', async () => {
|
|
422
|
+
// Arrange
|
|
423
|
+
const { storage, mockClient } = await createTestStorage();
|
|
424
|
+
// Act
|
|
425
|
+
await storage.unresolveEntry(1);
|
|
426
|
+
// Assert
|
|
427
|
+
expect(mockClient.hset).toHaveBeenCalledWith(expect.stringContaining('entries:1'), 'resolvedAt', '');
|
|
428
|
+
await storage.close();
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
// ==================== Family Hash Tests ====================
|
|
432
|
+
describe('updateFamilyHash', () => {
|
|
433
|
+
it('should update family hash and add to index', async () => {
|
|
434
|
+
// Arrange
|
|
435
|
+
const { storage, mockClient } = await createTestStorage();
|
|
436
|
+
// Act
|
|
437
|
+
await storage.updateFamilyHash(1, 'abc123');
|
|
438
|
+
// Assert
|
|
439
|
+
expect(mockClient.hset).toHaveBeenCalledWith(expect.stringContaining('entries:1'), 'familyHash', 'abc123');
|
|
440
|
+
expect(mockClient.sadd).toHaveBeenCalledWith(expect.stringContaining('family:abc123'), '1');
|
|
441
|
+
await storage.close();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
describe('findByFamilyHash', () => {
|
|
445
|
+
it('should find entries by family hash', async () => {
|
|
446
|
+
// Arrange
|
|
447
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
448
|
+
mockClient.smembers.mockResolvedValueOnce(['1', '2']); // family lookup
|
|
449
|
+
mockPipeline.exec.mockResolvedValue([
|
|
450
|
+
[null, createMockEntryHash(1, 'exception', { name: 'Error' })],
|
|
451
|
+
[null, createMockEntryHash(2, 'exception', { name: 'Error' })],
|
|
452
|
+
]);
|
|
453
|
+
mockClient.smembers.mockResolvedValue([]); // tag hydration
|
|
454
|
+
// Act
|
|
455
|
+
const entries = await storage.findByFamilyHash('abc123');
|
|
456
|
+
// Assert
|
|
457
|
+
expect(entries).toHaveLength(2);
|
|
458
|
+
await storage.close();
|
|
459
|
+
});
|
|
460
|
+
it('should return empty array when no entries found', async () => {
|
|
461
|
+
// Arrange
|
|
462
|
+
const { storage, mockClient } = await createTestStorage();
|
|
463
|
+
mockClient.smembers.mockResolvedValue([]);
|
|
464
|
+
// Act
|
|
465
|
+
const entries = await storage.findByFamilyHash('nonexistent');
|
|
466
|
+
// Assert
|
|
467
|
+
expect(entries).toEqual([]);
|
|
468
|
+
await storage.close();
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
// ==================== Statistics Tests ====================
|
|
472
|
+
describe('getStats', () => {
|
|
473
|
+
it('should return statistics', async () => {
|
|
474
|
+
// Arrange
|
|
475
|
+
const { storage, mockClient } = await createTestStorage();
|
|
476
|
+
mockClient.zcard
|
|
477
|
+
.mockResolvedValueOnce(10) // request
|
|
478
|
+
.mockResolvedValueOnce(5) // query
|
|
479
|
+
.mockResolvedValueOnce(2) // exception
|
|
480
|
+
.mockResolvedValue(0); // all others
|
|
481
|
+
// Act
|
|
482
|
+
const stats = await storage.getStats();
|
|
483
|
+
// Assert
|
|
484
|
+
expect(stats.total).toBe(17);
|
|
485
|
+
expect(stats.byType.request).toBe(10);
|
|
486
|
+
expect(stats.byType.query).toBe(5);
|
|
487
|
+
expect(stats.byType.exception).toBe(2);
|
|
488
|
+
await storage.close();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
describe('getStorageStats', () => {
|
|
492
|
+
it('should return storage statistics', async () => {
|
|
493
|
+
// Arrange
|
|
494
|
+
const { storage, mockClient } = await createTestStorage();
|
|
495
|
+
mockClient.zcard.mockResolvedValue(10);
|
|
496
|
+
mockClient.zrange.mockResolvedValue(['1']);
|
|
497
|
+
mockClient.zrevrange.mockResolvedValue(['10']);
|
|
498
|
+
mockClient.hget
|
|
499
|
+
.mockResolvedValueOnce('2024-01-01T00:00:00Z')
|
|
500
|
+
.mockResolvedValueOnce('2024-01-10T00:00:00Z');
|
|
501
|
+
// Act
|
|
502
|
+
const stats = await storage.getStorageStats();
|
|
503
|
+
// Assert
|
|
504
|
+
expect(stats.oldestEntry).toBe('2024-01-01T00:00:00Z');
|
|
505
|
+
expect(stats.newestEntry).toBe('2024-01-10T00:00:00Z');
|
|
506
|
+
await storage.close();
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
// ==================== Pruning Tests ====================
|
|
510
|
+
describe('prune', () => {
|
|
511
|
+
it('should delete entries older than given date', async () => {
|
|
512
|
+
// Arrange
|
|
513
|
+
const { storage, mockClient } = await createTestStorage();
|
|
514
|
+
mockClient.zrangebyscore.mockResolvedValue(['1', '2']);
|
|
515
|
+
mockClient.hgetall.mockResolvedValue(createMockEntryHash(1, 'request', {}));
|
|
516
|
+
mockClient.smembers.mockResolvedValue([]);
|
|
517
|
+
// Act
|
|
518
|
+
const deleted = await storage.prune(new Date('2024-01-01'));
|
|
519
|
+
// Assert
|
|
520
|
+
expect(deleted).toBe(2);
|
|
521
|
+
expect(mockClient.del).toHaveBeenCalled();
|
|
522
|
+
await storage.close();
|
|
523
|
+
});
|
|
524
|
+
it('should return 0 when no entries to prune', async () => {
|
|
525
|
+
// Arrange
|
|
526
|
+
const { storage, mockClient } = await createTestStorage();
|
|
527
|
+
mockClient.zrangebyscore.mockResolvedValue([]);
|
|
528
|
+
// Act
|
|
529
|
+
const deleted = await storage.prune(new Date('2024-01-01'));
|
|
530
|
+
// Assert
|
|
531
|
+
expect(deleted).toBe(0);
|
|
532
|
+
await storage.close();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
// ==================== Clear & Close Tests ====================
|
|
536
|
+
describe('clear', () => {
|
|
537
|
+
it('should delete all NestLens keys', async () => {
|
|
538
|
+
// Arrange
|
|
539
|
+
const { storage, mockClient } = await createTestStorage();
|
|
540
|
+
mockClient.keys.mockResolvedValue(['test:entries:1', 'test:entries:2']);
|
|
541
|
+
// Act
|
|
542
|
+
await storage.clear();
|
|
543
|
+
// Assert
|
|
544
|
+
expect(mockClient.del).toHaveBeenCalledWith('test:entries:1', 'test:entries:2');
|
|
545
|
+
await storage.close();
|
|
546
|
+
});
|
|
547
|
+
it('should handle empty storage', async () => {
|
|
548
|
+
// Arrange
|
|
549
|
+
const { storage, mockClient } = await createTestStorage();
|
|
550
|
+
mockClient.keys.mockResolvedValue([]);
|
|
551
|
+
// Act
|
|
552
|
+
await storage.clear();
|
|
553
|
+
// Assert
|
|
554
|
+
expect(mockClient.del).not.toHaveBeenCalled();
|
|
555
|
+
await storage.close();
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
describe('close', () => {
|
|
559
|
+
it('should close the Redis connection', async () => {
|
|
560
|
+
// Arrange
|
|
561
|
+
const { storage, mockClient } = await createTestStorage();
|
|
562
|
+
// Act
|
|
563
|
+
await storage.close();
|
|
564
|
+
// Assert
|
|
565
|
+
expect(mockClient.quit).toHaveBeenCalled();
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
// ==================== Sequence Tests ====================
|
|
569
|
+
describe('getLatestSequence', () => {
|
|
570
|
+
it('should return the latest sequence number', async () => {
|
|
571
|
+
// Arrange
|
|
572
|
+
const { storage, mockClient } = await createTestStorage();
|
|
573
|
+
mockClient.zrevrange.mockResolvedValue(['10']);
|
|
574
|
+
// Act
|
|
575
|
+
const sequence = await storage.getLatestSequence();
|
|
576
|
+
// Assert
|
|
577
|
+
expect(sequence).toBe(10);
|
|
578
|
+
await storage.close();
|
|
579
|
+
});
|
|
580
|
+
it('should return null for empty storage', async () => {
|
|
581
|
+
// Arrange
|
|
582
|
+
const { storage, mockClient } = await createTestStorage();
|
|
583
|
+
mockClient.zrevrange.mockResolvedValue([]);
|
|
584
|
+
// Act
|
|
585
|
+
const sequence = await storage.getLatestSequence();
|
|
586
|
+
// Assert
|
|
587
|
+
expect(sequence).toBeNull();
|
|
588
|
+
await storage.close();
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
describe('hasEntriesAfter', () => {
|
|
592
|
+
it('should return count of entries after sequence', async () => {
|
|
593
|
+
// Arrange
|
|
594
|
+
const { storage, mockClient } = await createTestStorage();
|
|
595
|
+
mockClient.zcount.mockResolvedValue(5);
|
|
596
|
+
// Act
|
|
597
|
+
const count = await storage.hasEntriesAfter(10);
|
|
598
|
+
// Assert
|
|
599
|
+
expect(count).toBe(5);
|
|
600
|
+
expect(mockClient.zcount).toHaveBeenCalledWith(expect.stringContaining('entries:all'), '(10', '+inf');
|
|
601
|
+
await storage.close();
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
// ==================== Cursor Pagination Tests ====================
|
|
605
|
+
describe('findWithCursor', () => {
|
|
606
|
+
it('should return entries with cursor metadata', async () => {
|
|
607
|
+
// Arrange
|
|
608
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
609
|
+
mockClient.zrevrange.mockResolvedValue(['2', '1']);
|
|
610
|
+
mockClient.zcard.mockResolvedValue(2);
|
|
611
|
+
mockPipeline.exec.mockResolvedValue([
|
|
612
|
+
[null, createMockEntryHash(2, 'request', { path: '/b' })],
|
|
613
|
+
[null, createMockEntryHash(1, 'request', { path: '/a' })],
|
|
614
|
+
]);
|
|
615
|
+
mockClient.smembers.mockResolvedValue([]);
|
|
616
|
+
// Act
|
|
617
|
+
const result = await storage.findWithCursor(undefined, { limit: 10 });
|
|
618
|
+
// Assert
|
|
619
|
+
expect(result.data).toHaveLength(2);
|
|
620
|
+
expect(result.meta.total).toBe(2);
|
|
621
|
+
expect(result.meta.newestSequence).toBe(2);
|
|
622
|
+
expect(result.meta.oldestSequence).toBe(1);
|
|
623
|
+
await storage.close();
|
|
624
|
+
});
|
|
625
|
+
it('should support beforeSequence pagination', async () => {
|
|
626
|
+
// Arrange
|
|
627
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
628
|
+
mockClient.zrevrangebyscore.mockResolvedValue(['1']);
|
|
629
|
+
mockClient.zcard.mockResolvedValue(2);
|
|
630
|
+
mockPipeline.exec.mockResolvedValue([
|
|
631
|
+
[null, createMockEntryHash(1, 'request', { path: '/a' })],
|
|
632
|
+
]);
|
|
633
|
+
mockClient.smembers.mockResolvedValue([]);
|
|
634
|
+
// Act
|
|
635
|
+
await storage.findWithCursor(undefined, {
|
|
636
|
+
limit: 10,
|
|
637
|
+
beforeSequence: 2,
|
|
638
|
+
});
|
|
639
|
+
// Assert
|
|
640
|
+
expect(mockClient.zrevrangebyscore).toHaveBeenCalledWith(expect.stringContaining('entries:all'), '(2', '-inf', 'LIMIT', '0', '11');
|
|
641
|
+
await storage.close();
|
|
642
|
+
});
|
|
643
|
+
it('should filter by type', async () => {
|
|
644
|
+
// Arrange
|
|
645
|
+
const { storage, mockClient, mockPipeline } = await createTestStorage();
|
|
646
|
+
mockClient.zrevrange.mockResolvedValue(['1']);
|
|
647
|
+
mockClient.zcard.mockResolvedValue(1);
|
|
648
|
+
mockPipeline.exec.mockResolvedValue([
|
|
649
|
+
[null, createMockEntryHash(1, 'query', { sql: 'SELECT 1' })],
|
|
650
|
+
]);
|
|
651
|
+
mockClient.smembers.mockResolvedValue([]);
|
|
652
|
+
// Act
|
|
653
|
+
await storage.findWithCursor('query', { limit: 10 });
|
|
654
|
+
// Assert
|
|
655
|
+
expect(mockClient.zrevrange).toHaveBeenCalledWith(expect.stringContaining('type:query'), 0, 10);
|
|
656
|
+
await storage.close();
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
//# sourceMappingURL=redis.storage.spec.js.map
|