@dangao/bun-server 1.8.0 → 1.8.2
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/docs/api.md +194 -81
- package/docs/extensions.md +53 -0
- package/docs/guide.md +243 -1
- package/docs/microservice-config-center.md +73 -74
- package/docs/microservice-nacos.md +89 -90
- package/docs/microservice-service-registry.md +85 -86
- package/docs/microservice.md +142 -137
- package/docs/request-lifecycle.md +45 -4
- package/docs/symbol-interface-pattern.md +106 -106
- package/docs/zh/api.md +458 -18
- package/docs/zh/extensions.md +53 -0
- package/docs/zh/guide.md +251 -4
- package/docs/zh/microservice-config-center.md +258 -0
- package/docs/zh/microservice-nacos.md +346 -0
- package/docs/zh/microservice-service-registry.md +306 -0
- package/docs/zh/microservice.md +680 -0
- package/docs/zh/request-lifecycle.md +43 -5
- package/package.json +1 -1
- package/tests/auth/auth-decorators.test.ts +241 -0
- package/tests/auth/oauth2-service.test.ts +318 -0
- package/tests/cache/cache-decorators-extended.test.ts +272 -0
- package/tests/cache/cache-interceptors.test.ts +534 -0
- package/tests/cache/cache-service-proxy.test.ts +246 -0
- package/tests/cache/memory-cache-store.test.ts +155 -0
- package/tests/cache/redis-cache-store.test.ts +199 -0
- package/tests/config/config-center-integration.test.ts +334 -0
- package/tests/config/config-module-extended.test.ts +165 -0
- package/tests/controller/param-binder.test.ts +333 -0
- package/tests/error/error-handler.test.ts +166 -57
- package/tests/error/i18n-extended.test.ts +105 -0
- package/tests/events/event-listener-scanner.test.ts +114 -0
- package/tests/events/event-module.test.ts +133 -302
- package/tests/extensions/logger-module.test.ts +158 -0
- package/tests/files/file-storage.test.ts +136 -0
- package/tests/interceptor/base-interceptor.test.ts +605 -0
- package/tests/interceptor/builtin/cache-interceptor.test.ts +233 -86
- package/tests/interceptor/builtin/log-interceptor.test.ts +469 -0
- package/tests/interceptor/builtin/permission-interceptor.test.ts +219 -120
- package/tests/interceptor/interceptor-chain.test.ts +241 -189
- package/tests/interceptor/interceptor-metadata.test.ts +221 -0
- package/tests/microservice/circuit-breaker.test.ts +221 -0
- package/tests/microservice/service-client-decorators.test.ts +86 -0
- package/tests/microservice/service-client-interceptors.test.ts +274 -0
- package/tests/microservice/service-registry-decorators.test.ts +147 -0
- package/tests/microservice/tracer.test.ts +213 -0
- package/tests/microservice/tracing-collectors.test.ts +168 -0
- package/tests/middleware/builtin/middleware-builtin-extended.test.ts +237 -0
- package/tests/middleware/builtin/rate-limit.test.ts +257 -0
- package/tests/middleware/middleware-decorators.test.ts +222 -0
- package/tests/middleware/middleware-pipeline.test.ts +160 -0
- package/tests/queue/queue-decorators.test.ts +139 -0
- package/tests/queue/queue-service.test.ts +191 -0
- package/tests/request/body-parser-extended.test.ts +291 -0
- package/tests/request/request-wrapper.test.ts +319 -0
- package/tests/router/router-decorators.test.ts +260 -0
- package/tests/router/router-extended.test.ts +298 -0
- package/tests/security/guards/reflector.test.ts +188 -0
- package/tests/security/security-filter.test.ts +182 -0
- package/tests/security/security-module-extended.test.ts +133 -0
- package/tests/session/memory-session-store.test.ts +172 -0
- package/tests/session/session-decorators.test.ts +163 -0
- package/tests/swagger/ui.test.ts +212 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CacheServiceProxy,
|
|
6
|
+
EnableCacheProxy,
|
|
7
|
+
isCacheProxyEnabled,
|
|
8
|
+
CachePostProcessor,
|
|
9
|
+
CACHE_PROXY_ENABLED_KEY,
|
|
10
|
+
} from '../../src/cache/service-proxy';
|
|
11
|
+
import {
|
|
12
|
+
Cacheable,
|
|
13
|
+
CacheEvict,
|
|
14
|
+
CachePut,
|
|
15
|
+
} from '../../src/cache/decorators';
|
|
16
|
+
import { CacheService } from '../../src/cache/service';
|
|
17
|
+
import { MemoryCacheStore, CACHE_SERVICE_TOKEN } from '../../src/cache/types';
|
|
18
|
+
import { Container } from '../../src/di/container';
|
|
19
|
+
|
|
20
|
+
describe('CacheServiceProxy', () => {
|
|
21
|
+
let container: Container;
|
|
22
|
+
let cacheService: CacheService;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
container = new Container();
|
|
26
|
+
const store = new MemoryCacheStore();
|
|
27
|
+
cacheService = new CacheService(store);
|
|
28
|
+
container.registerInstance(CACHE_SERVICE_TOKEN, cacheService);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('createProxy', () => {
|
|
32
|
+
test('should return original instance if no cache decorators', () => {
|
|
33
|
+
class NoCacheService {
|
|
34
|
+
public getValue(): string {
|
|
35
|
+
return 'value';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const instance = new NoCacheService();
|
|
40
|
+
const proxied = CacheServiceProxy.createProxy(instance, container);
|
|
41
|
+
|
|
42
|
+
// 如果没有缓存装饰器,应该返回原实例
|
|
43
|
+
expect(proxied).toBe(instance);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should create proxy for service with @Cacheable', async () => {
|
|
47
|
+
let callCount = 0;
|
|
48
|
+
|
|
49
|
+
class UserService {
|
|
50
|
+
@Cacheable({ key: 'user:{0}' })
|
|
51
|
+
public findById(id: string): string {
|
|
52
|
+
callCount++;
|
|
53
|
+
return `User-${id}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const instance = new UserService();
|
|
58
|
+
const proxied = CacheServiceProxy.createProxy(instance, container);
|
|
59
|
+
|
|
60
|
+
// 第一次调用应该执行方法
|
|
61
|
+
const result1 = await (proxied.findById as any)('123');
|
|
62
|
+
expect(result1).toBe('User-123');
|
|
63
|
+
expect(callCount).toBe(1);
|
|
64
|
+
|
|
65
|
+
// 第二次调用应该从缓存返回
|
|
66
|
+
const result2 = await (proxied.findById as any)('123');
|
|
67
|
+
expect(result2).toBe('User-123');
|
|
68
|
+
expect(callCount).toBe(1); // 不应该增加
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should create proxy for service with @CacheEvict', async () => {
|
|
72
|
+
class UserService {
|
|
73
|
+
@CacheEvict({ key: 'user:{0}' })
|
|
74
|
+
public deleteUser(id: string): string {
|
|
75
|
+
return 'deleted';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const instance = new UserService();
|
|
80
|
+
const proxied = CacheServiceProxy.createProxy(instance, container);
|
|
81
|
+
|
|
82
|
+
const result = await (proxied.deleteUser as any)('123');
|
|
83
|
+
expect(result).toBe('deleted');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should create proxy for service with @CacheEvict beforeInvocation', async () => {
|
|
87
|
+
class UserService {
|
|
88
|
+
@CacheEvict({ key: 'user:{0}', beforeInvocation: true })
|
|
89
|
+
public deleteUser(id: string): string {
|
|
90
|
+
return 'deleted';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const instance = new UserService();
|
|
95
|
+
const proxied = CacheServiceProxy.createProxy(instance, container);
|
|
96
|
+
|
|
97
|
+
const result = await (proxied.deleteUser as any)('123');
|
|
98
|
+
expect(result).toBe('deleted');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should create proxy for service with @CachePut', async () => {
|
|
102
|
+
class UserService {
|
|
103
|
+
@CachePut({ key: 'user:{0}' })
|
|
104
|
+
public updateUser(id: string): string {
|
|
105
|
+
return `Updated-${id}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const instance = new UserService();
|
|
110
|
+
const proxied = CacheServiceProxy.createProxy(instance, container);
|
|
111
|
+
|
|
112
|
+
// CachePut 总是执行方法并更新缓存
|
|
113
|
+
const result = await (proxied.updateUser as any)('123');
|
|
114
|
+
expect(result).toBe('Updated-123');
|
|
115
|
+
|
|
116
|
+
// 验证缓存被更新
|
|
117
|
+
const cached = await cacheService.get('user:123');
|
|
118
|
+
expect(cached).toBe('Updated-123');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('should handle non-function properties', () => {
|
|
122
|
+
class ServiceWithProps {
|
|
123
|
+
public name = 'TestService';
|
|
124
|
+
|
|
125
|
+
@Cacheable()
|
|
126
|
+
public getData(): string {
|
|
127
|
+
return 'data';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const instance = new ServiceWithProps();
|
|
132
|
+
const proxied = CacheServiceProxy.createProxy(instance, container);
|
|
133
|
+
|
|
134
|
+
// 非函数属性应该正常访问
|
|
135
|
+
expect(proxied.name).toBe('TestService');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should handle symbol properties', () => {
|
|
139
|
+
const sym = Symbol('test');
|
|
140
|
+
|
|
141
|
+
class ServiceWithSymbol {
|
|
142
|
+
public [sym](): string {
|
|
143
|
+
return 'symbol method';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Cacheable()
|
|
147
|
+
public getData(): string {
|
|
148
|
+
return 'data';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const instance = new ServiceWithSymbol();
|
|
153
|
+
const proxied = CacheServiceProxy.createProxy(instance, container);
|
|
154
|
+
|
|
155
|
+
// Symbol 属性的方法应该正常执行(不被拦截)
|
|
156
|
+
expect(proxied[sym]()).toBe('symbol method');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('EnableCacheProxy decorator', () => {
|
|
162
|
+
test('should set cache proxy enabled metadata', () => {
|
|
163
|
+
@EnableCacheProxy()
|
|
164
|
+
class TestService {}
|
|
165
|
+
|
|
166
|
+
const enabled = isCacheProxyEnabled(TestService);
|
|
167
|
+
expect(enabled).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('should return false for non-decorated class', () => {
|
|
171
|
+
class TestService {}
|
|
172
|
+
|
|
173
|
+
const enabled = isCacheProxyEnabled(TestService);
|
|
174
|
+
expect(enabled).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should set metadata with correct key', () => {
|
|
178
|
+
@EnableCacheProxy()
|
|
179
|
+
class TestService {}
|
|
180
|
+
|
|
181
|
+
const metadata = Reflect.getMetadata(CACHE_PROXY_ENABLED_KEY, TestService);
|
|
182
|
+
expect(metadata).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('CachePostProcessor', () => {
|
|
187
|
+
let container: Container;
|
|
188
|
+
let cacheService: CacheService;
|
|
189
|
+
let processor: CachePostProcessor;
|
|
190
|
+
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
container = new Container();
|
|
193
|
+
const store = new MemoryCacheStore();
|
|
194
|
+
cacheService = new CacheService(store);
|
|
195
|
+
container.registerInstance(CACHE_SERVICE_TOKEN, cacheService);
|
|
196
|
+
processor = new CachePostProcessor();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('should have correct priority', () => {
|
|
200
|
+
expect(processor.priority).toBe(50);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('should return original instance if not enabled', () => {
|
|
204
|
+
class RegularService {
|
|
205
|
+
public getData(): string {
|
|
206
|
+
return 'data';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const instance = new RegularService();
|
|
211
|
+
const result = processor.postProcess(instance, RegularService, container);
|
|
212
|
+
|
|
213
|
+
expect(result).toBe(instance);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('should create proxy for enabled service', () => {
|
|
217
|
+
@EnableCacheProxy()
|
|
218
|
+
class CachedService {
|
|
219
|
+
@Cacheable({ key: 'test' })
|
|
220
|
+
public getData(): string {
|
|
221
|
+
return 'data';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const instance = new CachedService();
|
|
226
|
+
const result = processor.postProcess(instance, CachedService, container);
|
|
227
|
+
|
|
228
|
+
// 应该返回代理而不是原实例
|
|
229
|
+
expect(result).not.toBe(instance);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('should return original if enabled but no cache decorators', () => {
|
|
233
|
+
@EnableCacheProxy()
|
|
234
|
+
class ServiceWithoutDecorators {
|
|
235
|
+
public getData(): string {
|
|
236
|
+
return 'data';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const instance = new ServiceWithoutDecorators();
|
|
241
|
+
const result = processor.postProcess(instance, ServiceWithoutDecorators, container);
|
|
242
|
+
|
|
243
|
+
// 没有缓存装饰器时,即使启用了代理也返回原实例
|
|
244
|
+
expect(result).toBe(instance);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { MemoryCacheStore } from '../../src/cache/types';
|
|
4
|
+
|
|
5
|
+
describe('MemoryCacheStore', () => {
|
|
6
|
+
let store: MemoryCacheStore;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
store = new MemoryCacheStore();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('get and set', () => {
|
|
13
|
+
test('should return undefined for non-existent key', async () => {
|
|
14
|
+
const value = await store.get('non-existent');
|
|
15
|
+
expect(value).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should set and get a value', async () => {
|
|
19
|
+
await store.set('key1', 'value1');
|
|
20
|
+
const value = await store.get('key1');
|
|
21
|
+
expect(value).toBe('value1');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should set and get an object', async () => {
|
|
25
|
+
const obj = { name: 'test', count: 42 };
|
|
26
|
+
await store.set('key2', obj);
|
|
27
|
+
const value = await store.get<typeof obj>('key2');
|
|
28
|
+
expect(value).toEqual(obj);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should return undefined for expired key', async () => {
|
|
32
|
+
await store.set('key3', 'value3', 50); // 50ms TTL
|
|
33
|
+
|
|
34
|
+
// 立即获取应该有值
|
|
35
|
+
const value1 = await store.get('key3');
|
|
36
|
+
expect(value1).toBe('value3');
|
|
37
|
+
|
|
38
|
+
// 等待过期
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
40
|
+
|
|
41
|
+
const value2 = await store.get('key3');
|
|
42
|
+
expect(value2).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should overwrite existing value', async () => {
|
|
46
|
+
await store.set('key4', 'first');
|
|
47
|
+
await store.set('key4', 'second');
|
|
48
|
+
const value = await store.get('key4');
|
|
49
|
+
expect(value).toBe('second');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('delete', () => {
|
|
54
|
+
test('should delete existing key', async () => {
|
|
55
|
+
await store.set('key5', 'value5');
|
|
56
|
+
const deleted = await store.delete('key5');
|
|
57
|
+
expect(deleted).toBe(true);
|
|
58
|
+
const value = await store.get('key5');
|
|
59
|
+
expect(value).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should return false for non-existent key', async () => {
|
|
63
|
+
const deleted = await store.delete('non-existent');
|
|
64
|
+
expect(deleted).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('has', () => {
|
|
69
|
+
test('should return true for existing key', async () => {
|
|
70
|
+
await store.set('key6', 'value6');
|
|
71
|
+
const exists = await store.has('key6');
|
|
72
|
+
expect(exists).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should return false for non-existent key', async () => {
|
|
76
|
+
const exists = await store.has('non-existent');
|
|
77
|
+
expect(exists).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should return false for expired key', async () => {
|
|
81
|
+
await store.set('key7', 'value7', 50);
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
83
|
+
const exists = await store.has('key7');
|
|
84
|
+
expect(exists).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('clear', () => {
|
|
89
|
+
test('should clear all entries', async () => {
|
|
90
|
+
await store.set('key8', 'value8');
|
|
91
|
+
await store.set('key9', 'value9');
|
|
92
|
+
await store.clear();
|
|
93
|
+
expect(await store.has('key8')).toBe(false);
|
|
94
|
+
expect(await store.has('key9')).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('getMany', () => {
|
|
99
|
+
test('should get multiple values', async () => {
|
|
100
|
+
await store.set('a', 1);
|
|
101
|
+
await store.set('b', 2);
|
|
102
|
+
await store.set('c', 3);
|
|
103
|
+
|
|
104
|
+
const result = await store.getMany<number>(['a', 'b', 'c', 'd']);
|
|
105
|
+
expect(result.get('a')).toBe(1);
|
|
106
|
+
expect(result.get('b')).toBe(2);
|
|
107
|
+
expect(result.get('c')).toBe(3);
|
|
108
|
+
expect(result.has('d')).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('setMany', () => {
|
|
113
|
+
test('should set multiple values', async () => {
|
|
114
|
+
const entries = [
|
|
115
|
+
{ key: 'x', value: 'X' },
|
|
116
|
+
{ key: 'y', value: 'Y' },
|
|
117
|
+
{ key: 'z', value: 'Z' },
|
|
118
|
+
];
|
|
119
|
+
await store.setMany(entries);
|
|
120
|
+
|
|
121
|
+
expect(await store.get('x')).toBe('X');
|
|
122
|
+
expect(await store.get('y')).toBe('Y');
|
|
123
|
+
expect(await store.get('z')).toBe('Z');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should set multiple values with TTL', async () => {
|
|
127
|
+
const entries = [
|
|
128
|
+
{ key: 'p', value: 'P' },
|
|
129
|
+
{ key: 'q', value: 'Q' },
|
|
130
|
+
];
|
|
131
|
+
await store.setMany(entries, 50);
|
|
132
|
+
|
|
133
|
+
expect(await store.get('p')).toBe('P');
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
135
|
+
expect(await store.get('p')).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('deleteMany', () => {
|
|
140
|
+
test('should delete multiple keys', async () => {
|
|
141
|
+
await store.set('m', 1);
|
|
142
|
+
await store.set('n', 2);
|
|
143
|
+
await store.set('o', 3);
|
|
144
|
+
|
|
145
|
+
const deleted = await store.deleteMany(['m', 'n', 'missing']);
|
|
146
|
+
expect(deleted).toContain('m');
|
|
147
|
+
expect(deleted).toContain('n');
|
|
148
|
+
expect(deleted).not.toContain('missing');
|
|
149
|
+
|
|
150
|
+
expect(await store.has('m')).toBe(false);
|
|
151
|
+
expect(await store.has('n')).toBe(false);
|
|
152
|
+
expect(await store.has('o')).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { RedisCacheStore, type RedisCacheStoreOptions } from '../../src/cache/types';
|
|
4
|
+
|
|
5
|
+
// Mock Redis client
|
|
6
|
+
function createMockRedisClient() {
|
|
7
|
+
const store = new Map<string, { value: string; expiresAt?: number }>();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
get: async (key: string): Promise<string | null> => {
|
|
11
|
+
const entry = store.get(key);
|
|
12
|
+
if (!entry) return null;
|
|
13
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
14
|
+
store.delete(key);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return entry.value;
|
|
18
|
+
},
|
|
19
|
+
set: async (key: string, value: string, options?: { PX?: number }): Promise<void> => {
|
|
20
|
+
const expiresAt = options?.PX ? Date.now() + options.PX : undefined;
|
|
21
|
+
store.set(key, { value, expiresAt });
|
|
22
|
+
},
|
|
23
|
+
del: async (key: string): Promise<void> => {
|
|
24
|
+
store.delete(key);
|
|
25
|
+
},
|
|
26
|
+
exists: async (key: string): Promise<number> => {
|
|
27
|
+
return store.has(key) ? 1 : 0;
|
|
28
|
+
},
|
|
29
|
+
mget: async (keys: string[]): Promise<(string | null)[]> => {
|
|
30
|
+
return keys.map((key) => {
|
|
31
|
+
const entry = store.get(key);
|
|
32
|
+
return entry ? entry.value : null;
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
mset: async (entries: Array<{ key: string; value: string }>): Promise<void> => {
|
|
36
|
+
for (const { key, value } of entries) {
|
|
37
|
+
store.set(key, { value });
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
flushdb: async (): Promise<void> => {
|
|
41
|
+
store.clear();
|
|
42
|
+
},
|
|
43
|
+
_store: store, // For testing
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('RedisCacheStore', () => {
|
|
48
|
+
let store: RedisCacheStore;
|
|
49
|
+
let mockClient: ReturnType<typeof createMockRedisClient>;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
mockClient = createMockRedisClient();
|
|
53
|
+
store = new RedisCacheStore({ client: mockClient });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('get', () => {
|
|
57
|
+
test('should return undefined for non-existent key', async () => {
|
|
58
|
+
const value = await store.get('non-existent');
|
|
59
|
+
expect(value).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should get and parse JSON value', async () => {
|
|
63
|
+
await mockClient.set('cache:test', JSON.stringify({ name: 'alice' }));
|
|
64
|
+
const value = await store.get<{ name: string }>('test');
|
|
65
|
+
expect(value).toEqual({ name: 'alice' });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should return undefined for invalid JSON', async () => {
|
|
69
|
+
await mockClient.set('cache:invalid', 'not-json');
|
|
70
|
+
const value = await store.get('invalid');
|
|
71
|
+
expect(value).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('set', () => {
|
|
76
|
+
test('should set value', async () => {
|
|
77
|
+
const result = await store.set('key1', { count: 42 });
|
|
78
|
+
expect(result).toBe(true);
|
|
79
|
+
|
|
80
|
+
const stored = await mockClient.get('cache:key1');
|
|
81
|
+
expect(stored).toBe(JSON.stringify({ count: 42 }));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should set value with TTL', async () => {
|
|
85
|
+
await store.set('key2', 'value', 1000);
|
|
86
|
+
|
|
87
|
+
const stored = await mockClient.get('cache:key2');
|
|
88
|
+
expect(stored).toBe('"value"');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('delete', () => {
|
|
93
|
+
test('should delete key', async () => {
|
|
94
|
+
await store.set('key3', 'value');
|
|
95
|
+
const result = await store.delete('key3');
|
|
96
|
+
expect(result).toBe(true);
|
|
97
|
+
|
|
98
|
+
const value = await store.get('key3');
|
|
99
|
+
expect(value).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('has', () => {
|
|
104
|
+
test('should return true for existing key', async () => {
|
|
105
|
+
await store.set('key4', 'value');
|
|
106
|
+
const exists = await store.has('key4');
|
|
107
|
+
expect(exists).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('should return false for non-existent key', async () => {
|
|
111
|
+
const exists = await store.has('non-existent');
|
|
112
|
+
expect(exists).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('clear', () => {
|
|
117
|
+
test('should clear all keys', async () => {
|
|
118
|
+
await store.set('a', 1);
|
|
119
|
+
await store.set('b', 2);
|
|
120
|
+
const result = await store.clear();
|
|
121
|
+
expect(result).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('getMany', () => {
|
|
126
|
+
test('should get multiple values', async () => {
|
|
127
|
+
await store.set('m1', 'v1');
|
|
128
|
+
await store.set('m2', 'v2');
|
|
129
|
+
|
|
130
|
+
const result = await store.getMany<string>(['m1', 'm2', 'm3']);
|
|
131
|
+
expect(result.get('m1')).toBe('v1');
|
|
132
|
+
expect(result.get('m2')).toBe('v2');
|
|
133
|
+
expect(result.has('m3')).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('should return empty map for empty keys', async () => {
|
|
137
|
+
const result = await store.getMany([]);
|
|
138
|
+
expect(result.size).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('setMany', () => {
|
|
143
|
+
test('should set multiple values', async () => {
|
|
144
|
+
const entries = [
|
|
145
|
+
{ key: 's1', value: 'v1' },
|
|
146
|
+
{ key: 's2', value: 'v2' },
|
|
147
|
+
];
|
|
148
|
+
const result = await store.setMany(entries);
|
|
149
|
+
expect(result).toBe(true);
|
|
150
|
+
|
|
151
|
+
expect(await store.get('s1')).toBe('v1');
|
|
152
|
+
expect(await store.get('s2')).toBe('v2');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('should return true for empty entries', async () => {
|
|
156
|
+
const result = await store.setMany([]);
|
|
157
|
+
expect(result).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('should set multiple values with TTL', async () => {
|
|
161
|
+
const entries = [
|
|
162
|
+
{ key: 't1', value: 'v1' },
|
|
163
|
+
{ key: 't2', value: 'v2' },
|
|
164
|
+
];
|
|
165
|
+
const result = await store.setMany(entries, 5000);
|
|
166
|
+
expect(result).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('deleteMany', () => {
|
|
171
|
+
test('should delete multiple keys', async () => {
|
|
172
|
+
await store.set('d1', 'v1');
|
|
173
|
+
await store.set('d2', 'v2');
|
|
174
|
+
|
|
175
|
+
const deleted = await store.deleteMany(['d1', 'd2', 'd3']);
|
|
176
|
+
expect(deleted).toContain('d1');
|
|
177
|
+
expect(deleted).toContain('d2');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('should return empty array for empty keys', async () => {
|
|
181
|
+
const deleted = await store.deleteMany([]);
|
|
182
|
+
expect(deleted).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('keyPrefix', () => {
|
|
187
|
+
test('should use custom key prefix', async () => {
|
|
188
|
+
const customStore = new RedisCacheStore({
|
|
189
|
+
client: mockClient,
|
|
190
|
+
keyPrefix: 'custom:',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await customStore.set('test', 'value');
|
|
194
|
+
|
|
195
|
+
const stored = await mockClient.get('custom:test');
|
|
196
|
+
expect(stored).toBe('"value"');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|