cleaned up stuff
This commit is contained in:
parent
a7146a3f57
commit
ac4c5078fa
9 changed files with 392 additions and 1204 deletions
|
|
@ -1,349 +0,0 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { QueueRateLimiter } from '../src/rate-limiter';
|
||||
import type { RateLimitRule } from '../src/types';
|
||||
|
||||
describe('QueueRateLimiter', () => {
|
||||
const mockRedisClient = {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
};
|
||||
|
||||
const mockLogger = {
|
||||
info: mock(() => {}),
|
||||
error: mock(() => {}),
|
||||
warn: mock(() => {}),
|
||||
debug: mock(() => {}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockLogger.info = mock(() => {});
|
||||
mockLogger.error = mock(() => {});
|
||||
mockLogger.warn = mock(() => {});
|
||||
mockLogger.debug = mock(() => {});
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create rate limiter', () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
expect(limiter).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRule', () => {
|
||||
it('should add a rate limit rule', () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const rule: RateLimitRule = {
|
||||
level: 'queue',
|
||||
queueName: 'test-queue',
|
||||
config: {
|
||||
points: 100,
|
||||
duration: 60,
|
||||
},
|
||||
};
|
||||
|
||||
limiter.addRule(rule);
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Rate limit rule added',
|
||||
expect.objectContaining({
|
||||
level: 'queue',
|
||||
queueName: 'test-queue',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should add operation-level rule', () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const rule: RateLimitRule = {
|
||||
level: 'operation',
|
||||
queueName: 'test-queue',
|
||||
handler: 'user-service',
|
||||
operation: 'process-user',
|
||||
config: {
|
||||
points: 10,
|
||||
duration: 60,
|
||||
blockDuration: 300,
|
||||
},
|
||||
};
|
||||
|
||||
limiter.addRule(rule);
|
||||
const rules = limiter.getRules();
|
||||
expect(rules).toHaveLength(1);
|
||||
expect(rules[0]).toEqual(rule);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkLimit', () => {
|
||||
it('should allow when no rules apply', async () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const result = await limiter.checkLimit('test-queue', 'handler', 'operation');
|
||||
expect(result.allowed).toBe(true);
|
||||
expect(result.appliedRule).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should check against global rule', async () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const globalRule: RateLimitRule = {
|
||||
level: 'global',
|
||||
config: { points: 1000, duration: 60 },
|
||||
};
|
||||
|
||||
limiter.addRule(globalRule);
|
||||
|
||||
const result = await limiter.checkLimit('any-queue', 'any-handler', 'any-op');
|
||||
// In test environment without real Redis, it returns allowed: true on error
|
||||
expect(result.allowed).toBe(true);
|
||||
// Check that error was logged
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'Rate limit check failed',
|
||||
expect.objectContaining({
|
||||
queueName: 'any-queue',
|
||||
handler: 'any-handler',
|
||||
operation: 'any-op',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should prefer more specific rules', async () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
// Add rules from least to most specific
|
||||
const globalRule: RateLimitRule = {
|
||||
level: 'global',
|
||||
config: { points: 1000, duration: 60 },
|
||||
};
|
||||
|
||||
const queueRule: RateLimitRule = {
|
||||
level: 'queue',
|
||||
queueName: 'test-queue',
|
||||
config: { points: 100, duration: 60 },
|
||||
};
|
||||
|
||||
const handlerRule: RateLimitRule = {
|
||||
level: 'handler',
|
||||
queueName: 'test-queue',
|
||||
handler: 'test-handler',
|
||||
config: { points: 50, duration: 60 },
|
||||
};
|
||||
|
||||
const operationRule: RateLimitRule = {
|
||||
level: 'operation',
|
||||
queueName: 'test-queue',
|
||||
handler: 'test-handler',
|
||||
operation: 'test-op',
|
||||
config: { points: 10, duration: 60 },
|
||||
};
|
||||
|
||||
limiter.addRule(globalRule);
|
||||
limiter.addRule(queueRule);
|
||||
limiter.addRule(handlerRule);
|
||||
limiter.addRule(operationRule);
|
||||
|
||||
// Operation level should take precedence
|
||||
const result = await limiter.checkLimit('test-queue', 'test-handler', 'test-op');
|
||||
expect(result.allowed).toBe(true);
|
||||
// Check that the most specific rule was attempted (operation level)
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'Rate limit check failed',
|
||||
expect.objectContaining({
|
||||
queueName: 'test-queue',
|
||||
handler: 'test-handler',
|
||||
operation: 'test-op',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('should get rate limit status', async () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const rule: RateLimitRule = {
|
||||
level: 'queue',
|
||||
queueName: 'test-queue',
|
||||
config: { points: 100, duration: 60 },
|
||||
};
|
||||
|
||||
limiter.addRule(rule);
|
||||
|
||||
const status = await limiter.getStatus('test-queue', 'handler', 'operation');
|
||||
|
||||
expect(status.queueName).toBe('test-queue');
|
||||
expect(status.handler).toBe('handler');
|
||||
expect(status.operation).toBe('operation');
|
||||
expect(status.appliedRule).toEqual(rule);
|
||||
});
|
||||
|
||||
it('should return status without rule when none apply', async () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const status = await limiter.getStatus('test-queue', 'handler', 'operation');
|
||||
|
||||
expect(status.queueName).toBe('test-queue');
|
||||
expect(status.appliedRule).toBeUndefined();
|
||||
expect(status.limit).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset rate limits for specific operation', async () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const rule: RateLimitRule = {
|
||||
level: 'operation',
|
||||
queueName: 'test-queue',
|
||||
handler: 'test-handler',
|
||||
operation: 'test-op',
|
||||
config: { points: 10, duration: 60 },
|
||||
};
|
||||
|
||||
limiter.addRule(rule);
|
||||
|
||||
try {
|
||||
await limiter.reset('test-queue', 'test-handler', 'test-op');
|
||||
} catch (error) {
|
||||
// In test environment, limiter.delete will fail due to no Redis connection
|
||||
// That's expected, just ensure the method can be called
|
||||
}
|
||||
|
||||
// The method should at least attempt to reset
|
||||
expect(limiter.getRules()).toContain(rule);
|
||||
});
|
||||
|
||||
it('should warn about broad reset', async () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
await limiter.reset('test-queue');
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
'Broad reset not implemented yet',
|
||||
expect.objectContaining({ queueName: 'test-queue' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeRule', () => {
|
||||
it('should remove a rule', () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const rule: RateLimitRule = {
|
||||
level: 'queue',
|
||||
queueName: 'test-queue',
|
||||
config: { points: 100, duration: 60 },
|
||||
};
|
||||
|
||||
limiter.addRule(rule);
|
||||
expect(limiter.getRules()).toHaveLength(1);
|
||||
|
||||
const removed = limiter.removeRule('queue', 'test-queue');
|
||||
expect(removed).toBe(true);
|
||||
expect(limiter.getRules()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return false when rule not found', () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const removed = limiter.removeRule('queue', 'non-existent');
|
||||
expect(removed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRules', () => {
|
||||
it('should return all configured rules', () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
const rule1: RateLimitRule = {
|
||||
level: 'global',
|
||||
config: { points: 1000, duration: 60 },
|
||||
};
|
||||
|
||||
const rule2: RateLimitRule = {
|
||||
level: 'queue',
|
||||
queueName: 'test-queue',
|
||||
config: { points: 100, duration: 60 },
|
||||
};
|
||||
|
||||
limiter.addRule(rule1);
|
||||
limiter.addRule(rule2);
|
||||
|
||||
const rules = limiter.getRules();
|
||||
expect(rules).toHaveLength(2);
|
||||
expect(rules).toContain(rule1);
|
||||
expect(rules).toContain(rule2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should allow on rate limiter error', async () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
// Add a rule but don't set up the actual limiter to cause an error
|
||||
const rule: RateLimitRule = {
|
||||
level: 'queue',
|
||||
queueName: 'test-queue',
|
||||
config: { points: 100, duration: 60 },
|
||||
};
|
||||
|
||||
limiter.addRule(rule);
|
||||
|
||||
// Force the limiter map to be empty to simulate error
|
||||
(limiter as any).limiters.clear();
|
||||
|
||||
const result = await limiter.checkLimit('test-queue', 'handler', 'operation');
|
||||
|
||||
expect(result.allowed).toBe(true); // Should allow on error
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
'Rate limiter not found for rule',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hierarchical rule precedence', () => {
|
||||
it('should correctly apply rule hierarchy', () => {
|
||||
const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
|
||||
|
||||
// Add multiple rules at different levels
|
||||
const rules: RateLimitRule[] = [
|
||||
{
|
||||
level: 'global',
|
||||
config: { points: 10000, duration: 60 },
|
||||
},
|
||||
{
|
||||
level: 'queue',
|
||||
queueName: 'email-queue',
|
||||
config: { points: 1000, duration: 60 },
|
||||
},
|
||||
{
|
||||
level: 'handler',
|
||||
queueName: 'email-queue',
|
||||
handler: 'email-service',
|
||||
config: { points: 100, duration: 60 },
|
||||
},
|
||||
{
|
||||
level: 'operation',
|
||||
queueName: 'email-queue',
|
||||
handler: 'email-service',
|
||||
operation: 'send-bulk',
|
||||
config: { points: 10, duration: 60 },
|
||||
},
|
||||
];
|
||||
|
||||
rules.forEach(rule => limiter.addRule(rule));
|
||||
|
||||
// Test that getMostSpecificRule works correctly
|
||||
const specificRule = (limiter as any).getMostSpecificRule(
|
||||
'email-queue',
|
||||
'email-service',
|
||||
'send-bulk'
|
||||
);
|
||||
|
||||
expect(specificRule?.level).toBe('operation');
|
||||
expect(specificRule?.config.points).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue