queen-mq 0.3.1 → 0.6.3
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/README.md +309 -167
- package/client-js/client-v2/Queen.js +66 -0
- package/client-js/client-v2/README.md +67 -10
- package/client-js/client-v2/builders/QueueBuilder.js +7 -1
- package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
- package/client-js/client-v2/stream/StreamBuilder.js +72 -0
- package/client-js/client-v2/stream/StreamConsumer.js +156 -0
- package/client-js/client-v2/stream/Window.js +140 -0
- package/client-js/test-v2/GETTING_STARTED.md +27 -0
- package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
- package/client-js/test-v2/README_SUBSCRIPTION_TESTS.md +201 -0
- package/client-js/test-v2/consume.js +11 -0
- package/client-js/test-v2/dlq.js +1 -1
- package/client-js/test-v2/load.js +2 -0
- package/client-js/test-v2/maintenance.js +261 -0
- package/client-js/test-v2/retention.js +69 -0
- package/client-js/test-v2/run.js +7 -1
- package/client-js/test-v2/subscription.js +198 -18
- package/client-js/test-v2/transaction.js +66 -4
- package/package.json +2 -2
- package/client-js/client/client.js +0 -1536
- package/client-js/client/index.js +0 -4
- package/client-js/client/utils/http.js +0 -173
- package/client-js/client/utils/loadBalancer.js +0 -152
- package/client-js/client/utils/retry.js +0 -41
- package/client-js/services/encryptionService.js +0 -82
- package/client-js/services/evictionService.js +0 -160
- package/client-js/services/retentionService.js +0 -162
- package/client-js/services/startupSync.js +0 -35
- package/client-js/test/README.md +0 -224
- package/client-js/test/advanced-client-tests.js +0 -761
- package/client-js/test/advanced-pattern-tests.js +0 -1137
- package/client-js/test/bus-mode-tests.js +0 -361
- package/client-js/test/core-tests.js +0 -457
- package/client-js/test/edge-case-tests.js +0 -562
- package/client-js/test/enterprise-tests.js +0 -637
- package/client-js/test/human.js +0 -162
- package/client-js/test/partition-locking-tests.js +0 -545
- package/client-js/test/partition-transaction-tests.js +0 -482
- package/client-js/test/qos0-tests.js +0 -334
- package/client-js/test/test-new.js +0 -370
- package/client-js/test/utils.js +0 -169
- package/client-js/test/window-buffer-test.js +0 -114
- package/client-js/utils/logger.js +0 -44
- package/client-js/utils/uuid.js +0 -5
|
@@ -1,637 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enterprise Feature Tests for Queen Message Queue
|
|
3
|
-
* Tests: Encryption, retention policies, message eviction, combined enterprise features
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { startTest, passTest, failTest, sleep, dbPool, getMessageCount, log } from './utils.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Test: Message Encryption
|
|
10
|
-
*/
|
|
11
|
-
export async function testMessageEncryption(client) {
|
|
12
|
-
startTest('Message Encryption', 'enterprise');
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
// Check if encryption is enabled
|
|
16
|
-
const encryptionKey = process.env.QUEEN_ENCRYPTION_KEY;
|
|
17
|
-
if (!encryptionKey) {
|
|
18
|
-
log('Skipping encryption test - QUEEN_ENCRYPTION_KEY not set', 'warning');
|
|
19
|
-
passTest('Skipped - encryption key not configured');
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const queue = 'test-encryption-queue';
|
|
24
|
-
|
|
25
|
-
// Configure queue with encryption
|
|
26
|
-
await client.queue(queue, {
|
|
27
|
-
encryptionEnabled: true
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// Verify encryption is enabled in database
|
|
31
|
-
const queueResult = await dbPool.query(`
|
|
32
|
-
SELECT encryption_enabled
|
|
33
|
-
FROM queen.queues
|
|
34
|
-
WHERE name = $1
|
|
35
|
-
`, [queue]);
|
|
36
|
-
|
|
37
|
-
if (!queueResult.rows[0]?.encryption_enabled) {
|
|
38
|
-
throw new Error('Encryption not enabled on queue');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Push sensitive data
|
|
42
|
-
const sensitiveData = {
|
|
43
|
-
creditCard: '4111111111111111',
|
|
44
|
-
ssn: '123-45-6789',
|
|
45
|
-
password: 'super-secret-password',
|
|
46
|
-
timestamp: Date.now()
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
await client.push(queue, sensitiveData);
|
|
50
|
-
await sleep(100);
|
|
51
|
-
|
|
52
|
-
// Check that the message is encrypted in database
|
|
53
|
-
const dbResult = await dbPool.query(`
|
|
54
|
-
SELECT m.payload, m.is_encrypted
|
|
55
|
-
FROM queen.messages m
|
|
56
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
57
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
58
|
-
WHERE q.name = $1
|
|
59
|
-
ORDER BY m.created_at DESC
|
|
60
|
-
LIMIT 1
|
|
61
|
-
`, [queue]);
|
|
62
|
-
|
|
63
|
-
if (!dbResult.rows[0]?.is_encrypted) {
|
|
64
|
-
throw new Error('Message not marked as encrypted in database');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const storedData = dbResult.rows[0].payload;
|
|
68
|
-
|
|
69
|
-
// Verify the stored data is encrypted
|
|
70
|
-
if (!storedData.encrypted || !storedData.iv || !storedData.authTag) {
|
|
71
|
-
throw new Error('Message not properly encrypted in database');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Verify the stored data doesn't contain plain text sensitive info
|
|
75
|
-
const storedStr = JSON.stringify(storedData);
|
|
76
|
-
if (storedStr.includes('4111111111111111') ||
|
|
77
|
-
storedStr.includes('123-45-6789') ||
|
|
78
|
-
storedStr.includes('super-secret-password')) {
|
|
79
|
-
throw new Error('Sensitive data found in plain text - encryption failed!');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Take the message (should be decrypted automatically)
|
|
83
|
-
let decryptedData = null;
|
|
84
|
-
for await (const msg of client.take(queue, { limit: 1 })) {
|
|
85
|
-
decryptedData = msg.data;
|
|
86
|
-
await client.ack(msg);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!decryptedData) {
|
|
90
|
-
throw new Error('Failed to take encrypted message');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Verify decrypted data matches original
|
|
94
|
-
if (decryptedData.creditCard !== sensitiveData.creditCard ||
|
|
95
|
-
decryptedData.ssn !== sensitiveData.ssn ||
|
|
96
|
-
decryptedData.password !== sensitiveData.password) {
|
|
97
|
-
throw new Error('Decrypted data does not match original');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
passTest('Message encryption/decryption works correctly');
|
|
101
|
-
} catch (error) {
|
|
102
|
-
failTest(error);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Test: Retention Policy - Pending Messages
|
|
108
|
-
*/
|
|
109
|
-
export async function testRetentionPendingMessages(client) {
|
|
110
|
-
startTest('Retention Policy - Pending Messages', 'enterprise');
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const queue = 'test-retention-pending';
|
|
114
|
-
|
|
115
|
-
// Configure queue with retention for pending messages
|
|
116
|
-
await client.queue(queue, {
|
|
117
|
-
retentionEnabled: true,
|
|
118
|
-
retentionSeconds: 3 // 3 seconds retention for pending messages
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Push messages
|
|
122
|
-
await client.push(queue, [
|
|
123
|
-
{ message: 'Message to be retained', order: 1 },
|
|
124
|
-
{ message: 'Another message to be retained', order: 2 }
|
|
125
|
-
]);
|
|
126
|
-
|
|
127
|
-
// Verify messages are in database
|
|
128
|
-
const initialCount = await getMessageCount(queue, 'Default');
|
|
129
|
-
if (initialCount !== 2) {
|
|
130
|
-
throw new Error(`Expected 2 messages, got ${initialCount}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Wait for retention period
|
|
134
|
-
log('Waiting for retention period (3 seconds)...', 'info');
|
|
135
|
-
await sleep(3500);
|
|
136
|
-
|
|
137
|
-
// Trigger retention by attempting a take
|
|
138
|
-
for await (const msg of client.take(queue, { limit: 1 })) {
|
|
139
|
-
// Should get nothing or expired messages
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Check if messages were retained
|
|
143
|
-
const finalCount = await getMessageCount(queue, 'Default');
|
|
144
|
-
log(`Messages after retention period: ${finalCount} (from ${initialCount})`, 'info');
|
|
145
|
-
|
|
146
|
-
// Check retention history
|
|
147
|
-
const retentionHistory = await dbPool.query(`
|
|
148
|
-
SELECT rh.messages_deleted, rh.retention_type
|
|
149
|
-
FROM queen.retention_history rh
|
|
150
|
-
JOIN queen.partitions p ON rh.partition_id = p.id
|
|
151
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
152
|
-
WHERE q.name = $1
|
|
153
|
-
ORDER BY rh.executed_at DESC
|
|
154
|
-
LIMIT 1
|
|
155
|
-
`, [queue]);
|
|
156
|
-
|
|
157
|
-
if (retentionHistory.rows.length > 0) {
|
|
158
|
-
log(`Retention history: ${retentionHistory.rows[0].messages_deleted} messages deleted (${retentionHistory.rows[0].retention_type})`, 'info');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
passTest('Retention policy for pending messages configured successfully');
|
|
162
|
-
} catch (error) {
|
|
163
|
-
failTest(error);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Test: Message Eviction
|
|
169
|
-
*/
|
|
170
|
-
export async function testMessageEviction(client) {
|
|
171
|
-
startTest('Message Eviction', 'enterprise');
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
const queue = 'test-eviction-queue';
|
|
175
|
-
|
|
176
|
-
// Configure queue with max wait time
|
|
177
|
-
await client.queue(queue, {
|
|
178
|
-
maxWaitTimeSeconds: 3
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Push message
|
|
182
|
-
await client.push(`${queue}/eviction-partition`, {
|
|
183
|
-
message: 'Message to be evicted',
|
|
184
|
-
timestamp: Date.now()
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Verify message exists and is pending
|
|
188
|
-
const pendingCheck = await dbPool.query(`
|
|
189
|
-
SELECT m.id, m.created_at,
|
|
190
|
-
pc.last_consumed_id, pc.last_consumed_created_at,
|
|
191
|
-
dlq.id as dlq_id
|
|
192
|
-
FROM queen.messages m
|
|
193
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
194
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
195
|
-
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
|
|
196
|
-
LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id AND dlq.consumer_group = '__QUEUE_MODE__'
|
|
197
|
-
WHERE q.name = $1 AND p.name = 'eviction-partition'
|
|
198
|
-
ORDER BY m.created_at DESC
|
|
199
|
-
LIMIT 1
|
|
200
|
-
`, [queue]);
|
|
201
|
-
|
|
202
|
-
if (pendingCheck.rows.length === 0) {
|
|
203
|
-
throw new Error('Message not found');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Derive status from cursor position
|
|
207
|
-
const msg = pendingCheck.rows[0];
|
|
208
|
-
const isDLQ = msg.dlq_id != null;
|
|
209
|
-
const isConsumed = msg.last_consumed_id &&
|
|
210
|
-
((msg.created_at < msg.last_consumed_created_at) ||
|
|
211
|
-
(msg.created_at.getTime() === msg.last_consumed_created_at?.getTime() && msg.id <= msg.last_consumed_id));
|
|
212
|
-
const status = isDLQ ? 'dead_letter' : (isConsumed ? 'completed' : 'pending');
|
|
213
|
-
|
|
214
|
-
if (status !== 'pending') {
|
|
215
|
-
throw new Error(`Message not in pending status, got: ${status}`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Wait for max wait time to expire
|
|
219
|
-
log('Waiting for max wait time (3 seconds)...', 'info');
|
|
220
|
-
await sleep(3500);
|
|
221
|
-
|
|
222
|
-
// Try to take (should trigger eviction)
|
|
223
|
-
let gotMessage = false;
|
|
224
|
-
for await (const msg of client.take(`${queue}/eviction-partition`, { limit: 1 })) {
|
|
225
|
-
gotMessage = true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Check if message was evicted (moved to DLQ)
|
|
229
|
-
const evictionCheck = await dbPool.query(`
|
|
230
|
-
SELECT m.id, dlq.error_message,
|
|
231
|
-
CASE WHEN dlq.id IS NOT NULL THEN 'evicted' ELSE 'pending' END as status
|
|
232
|
-
FROM queen.messages m
|
|
233
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
234
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
235
|
-
LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id AND dlq.consumer_group = '__QUEUE_MODE__'
|
|
236
|
-
WHERE q.name = $1 AND p.name = 'eviction-partition'
|
|
237
|
-
ORDER BY m.created_at DESC
|
|
238
|
-
LIMIT 1
|
|
239
|
-
`, [queue]);
|
|
240
|
-
|
|
241
|
-
if (evictionCheck.rows.length > 0) {
|
|
242
|
-
const status = evictionCheck.rows[0].status;
|
|
243
|
-
const errorMsg = evictionCheck.rows[0].error_message;
|
|
244
|
-
|
|
245
|
-
if (status === 'evicted') {
|
|
246
|
-
log(`Message evicted with reason: ${errorMsg}`, 'info');
|
|
247
|
-
passTest('Message eviction works correctly');
|
|
248
|
-
} else if (!gotMessage) {
|
|
249
|
-
log('Message may be evicted by background service', 'info');
|
|
250
|
-
passTest('Eviction configured successfully');
|
|
251
|
-
} else {
|
|
252
|
-
passTest('Eviction configuration accepted');
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
passTest('Message processed or evicted');
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
failTest(error);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Test: Combined Enterprise Features
|
|
264
|
-
*/
|
|
265
|
-
export async function testCombinedEnterpriseFeatures(client) {
|
|
266
|
-
startTest('Combined Enterprise Features', 'enterprise');
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
const queue = 'test-enterprise-combined';
|
|
270
|
-
const encryptionEnabled = !!process.env.QUEEN_ENCRYPTION_KEY;
|
|
271
|
-
|
|
272
|
-
// Configure queue with all enterprise features
|
|
273
|
-
await client.queue(queue, {
|
|
274
|
-
// Core options
|
|
275
|
-
leaseTime: 300,
|
|
276
|
-
retryLimit: 3,
|
|
277
|
-
priority: 10,
|
|
278
|
-
|
|
279
|
-
// Enterprise options
|
|
280
|
-
encryptionEnabled: encryptionEnabled,
|
|
281
|
-
retentionEnabled: true,
|
|
282
|
-
retentionSeconds: 60,
|
|
283
|
-
completedRetentionSeconds: 30,
|
|
284
|
-
partitionRetentionSeconds: 120
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// Also set max wait time on the queue
|
|
288
|
-
await dbPool.query(`
|
|
289
|
-
UPDATE queen.queues
|
|
290
|
-
SET max_wait_time_seconds = 45
|
|
291
|
-
WHERE name = $1
|
|
292
|
-
`, [queue]);
|
|
293
|
-
|
|
294
|
-
// Verify all configurations
|
|
295
|
-
const configCheck = await dbPool.query(`
|
|
296
|
-
SELECT
|
|
297
|
-
q.encryption_enabled,
|
|
298
|
-
q.max_wait_time_seconds,
|
|
299
|
-
q.retention_enabled,
|
|
300
|
-
q.retention_seconds,
|
|
301
|
-
q.completed_retention_seconds,
|
|
302
|
-
q.lease_time,
|
|
303
|
-
q.retry_limit,
|
|
304
|
-
q.priority
|
|
305
|
-
FROM queen.queues q
|
|
306
|
-
WHERE q.name = $1
|
|
307
|
-
`, [queue]);
|
|
308
|
-
|
|
309
|
-
if (configCheck.rows.length === 0) {
|
|
310
|
-
throw new Error('Configuration not found');
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const config = configCheck.rows[0];
|
|
314
|
-
|
|
315
|
-
// Verify enterprise configurations
|
|
316
|
-
if (encryptionEnabled && config.encryption_enabled !== encryptionEnabled) {
|
|
317
|
-
throw new Error('Encryption not configured correctly');
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (config.max_wait_time_seconds !== 45) {
|
|
321
|
-
throw new Error('Max wait time not configured correctly');
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (!config.retention_enabled ||
|
|
325
|
-
config.retention_seconds !== 60 ||
|
|
326
|
-
config.completed_retention_seconds !== 30) {
|
|
327
|
-
throw new Error('Retention options not configured correctly');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Push a test message
|
|
331
|
-
await client.push(queue, {
|
|
332
|
-
message: 'Enterprise test message',
|
|
333
|
-
sensitive: 'confidential-data',
|
|
334
|
-
timestamp: Date.now()
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
await sleep(100);
|
|
338
|
-
|
|
339
|
-
// If encryption is enabled, verify message is encrypted in DB
|
|
340
|
-
if (encryptionEnabled) {
|
|
341
|
-
const encCheck = await dbPool.query(`
|
|
342
|
-
SELECT is_encrypted FROM queen.messages m
|
|
343
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
344
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
345
|
-
WHERE q.name = $1
|
|
346
|
-
ORDER BY m.created_at DESC
|
|
347
|
-
LIMIT 1
|
|
348
|
-
`, [queue]);
|
|
349
|
-
|
|
350
|
-
if (encCheck.rows[0] && !encCheck.rows[0].is_encrypted) {
|
|
351
|
-
throw new Error('Message not encrypted despite encryption being enabled');
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Take and complete the message
|
|
356
|
-
for await (const msg of client.take(queue, { limit: 1 })) {
|
|
357
|
-
await client.ack(msg);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
passTest('All enterprise features can be configured together successfully');
|
|
361
|
-
} catch (error) {
|
|
362
|
-
failTest(error);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Test: Retention Policy - Completed Messages
|
|
368
|
-
*/
|
|
369
|
-
export async function testRetentionCompletedMessages(client) {
|
|
370
|
-
startTest('Retention Policy - Completed Messages', 'enterprise');
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
const queue = 'test-retention-completed';
|
|
374
|
-
|
|
375
|
-
// Configure queue with retention for completed messages
|
|
376
|
-
await client.queue(queue, {
|
|
377
|
-
retentionEnabled: true,
|
|
378
|
-
completedRetentionSeconds: 2 // 2 seconds retention for completed messages
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// Push messages
|
|
382
|
-
await client.push(queue, { message: 'Message to complete and retain' });
|
|
383
|
-
await sleep(100);
|
|
384
|
-
|
|
385
|
-
// Take and complete the message
|
|
386
|
-
let transactionId;
|
|
387
|
-
for await (const msg of client.take(queue, { limit: 1 })) {
|
|
388
|
-
transactionId = msg.transactionId;
|
|
389
|
-
await client.ack(msg);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (!transactionId) {
|
|
393
|
-
throw new Error('Failed to take message for completion');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Verify message is marked as completed (cursor-based check using UUIDv7 ordering)
|
|
397
|
-
const statusCheck = await dbPool.query(`
|
|
398
|
-
SELECT
|
|
399
|
-
m.id, m.created_at,
|
|
400
|
-
pc.last_consumed_id, pc.last_consumed_created_at,
|
|
401
|
-
CASE
|
|
402
|
-
WHEN pc.last_consumed_id IS NOT NULL AND m.id <= pc.last_consumed_id
|
|
403
|
-
THEN 'completed'
|
|
404
|
-
ELSE 'pending'
|
|
405
|
-
END as status
|
|
406
|
-
FROM queen.messages m
|
|
407
|
-
LEFT JOIN queen.partition_consumers pc
|
|
408
|
-
ON pc.partition_id = m.partition_id
|
|
409
|
-
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
410
|
-
WHERE m.transaction_id = $1
|
|
411
|
-
`, [transactionId]);
|
|
412
|
-
|
|
413
|
-
if (!statusCheck.rows[0] || statusCheck.rows[0].status !== 'completed') {
|
|
414
|
-
const row = statusCheck.rows[0];
|
|
415
|
-
throw new Error(
|
|
416
|
-
`Message not marked as completed.\n` +
|
|
417
|
-
` Status: ${row?.status}\n` +
|
|
418
|
-
` Message ID: ${row?.id}\n` +
|
|
419
|
-
` Cursor ID: ${row?.last_consumed_id}\n` +
|
|
420
|
-
` Has cursor: ${row?.last_consumed_id != null}`
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Wait for retention period
|
|
425
|
-
log('Waiting for completed retention period (2 seconds)...', 'info');
|
|
426
|
-
await sleep(2500);
|
|
427
|
-
|
|
428
|
-
// Trigger retention check
|
|
429
|
-
for await (const msg of client.take(queue, { limit: 1 })) {
|
|
430
|
-
// Should get nothing
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Check if completed message still exists
|
|
434
|
-
const retainedCheck = await dbPool.query(`
|
|
435
|
-
SELECT m.id,
|
|
436
|
-
CASE
|
|
437
|
-
WHEN (m.created_at, m.id) <= (pc.last_consumed_created_at, COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
|
|
438
|
-
THEN 'completed'
|
|
439
|
-
ELSE 'pending'
|
|
440
|
-
END as status,
|
|
441
|
-
pc.last_consumed_at as completed_at
|
|
442
|
-
FROM queen.messages m
|
|
443
|
-
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id AND pc.consumer_group = '__QUEUE_MODE__'
|
|
444
|
-
WHERE m.transaction_id = $1
|
|
445
|
-
`, [transactionId]);
|
|
446
|
-
|
|
447
|
-
const messageExists = retainedCheck.rows.length > 0;
|
|
448
|
-
const isCompleted = retainedCheck.rows[0]?.status === 'completed';
|
|
449
|
-
|
|
450
|
-
if (!messageExists) {
|
|
451
|
-
log('Message was deleted after retention period', 'info');
|
|
452
|
-
} else if (isCompleted) {
|
|
453
|
-
log('Message retained with completed status', 'info');
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
passTest('Retention policy for completed messages works correctly');
|
|
457
|
-
} catch (error) {
|
|
458
|
-
failTest(error);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Test: Partition Retention
|
|
464
|
-
*/
|
|
465
|
-
export async function testPartitionRetention(client) {
|
|
466
|
-
startTest('Partition Management', 'enterprise');
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
const queue = 'test-partition-management';
|
|
470
|
-
|
|
471
|
-
// Configure queue
|
|
472
|
-
await client.queue(queue, {
|
|
473
|
-
retentionEnabled: false
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// Push a message to create Default partition
|
|
477
|
-
await client.push(queue, { message: 'Test message' });
|
|
478
|
-
|
|
479
|
-
// Verify partition exists
|
|
480
|
-
const partitionCheck = await dbPool.query(`
|
|
481
|
-
SELECT p.id, p.last_activity
|
|
482
|
-
FROM queen.partitions p
|
|
483
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
484
|
-
WHERE q.name = $1 AND p.name = 'Default'
|
|
485
|
-
`, [queue]);
|
|
486
|
-
|
|
487
|
-
if (partitionCheck.rows.length === 0) {
|
|
488
|
-
throw new Error('Default partition not created');
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const partitionId = partitionCheck.rows[0].id;
|
|
492
|
-
log(`Partition created with ID: ${partitionId}`, 'info');
|
|
493
|
-
|
|
494
|
-
// Push another message to update last_activity
|
|
495
|
-
await client.push(queue, { message: 'Another message' });
|
|
496
|
-
|
|
497
|
-
// Take and complete the messages
|
|
498
|
-
for await (const msg of client.take(queue, { limit: 2 })) {
|
|
499
|
-
await client.ack(msg);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Check partition still exists
|
|
503
|
-
const stillExists = await dbPool.query(`
|
|
504
|
-
SELECT COUNT(*) as count FROM queen.partitions WHERE id = $1
|
|
505
|
-
`, [partitionId]);
|
|
506
|
-
|
|
507
|
-
if (stillExists.rows[0].count === '0') {
|
|
508
|
-
throw new Error('Partition deleted too early');
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
passTest('Partition management works correctly');
|
|
512
|
-
} catch (error) {
|
|
513
|
-
failTest(error);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Test: Consumer Encryption/Decryption
|
|
519
|
-
*/
|
|
520
|
-
export async function testConsumerEncryption(client) {
|
|
521
|
-
startTest('Consumer Encryption/Decryption', 'enterprise');
|
|
522
|
-
|
|
523
|
-
try {
|
|
524
|
-
// Check if encryption is enabled
|
|
525
|
-
const encryptionKey = process.env.QUEEN_ENCRYPTION_KEY;
|
|
526
|
-
if (!encryptionKey) {
|
|
527
|
-
log('Skipping consumer encryption test - QUEEN_ENCRYPTION_KEY not set', 'warning');
|
|
528
|
-
passTest('Skipped - encryption key not configured');
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const queue = 'test-consumer-encryption';
|
|
533
|
-
|
|
534
|
-
// Configure queue with encryption
|
|
535
|
-
await client.queue(queue, {
|
|
536
|
-
encryptionEnabled: true
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Push sensitive data
|
|
540
|
-
const sensitiveData = {
|
|
541
|
-
creditCard: '4111111111111111',
|
|
542
|
-
ssn: '123-45-6789',
|
|
543
|
-
apiKey: 'sk_test_12345',
|
|
544
|
-
timestamp: Date.now()
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
await client.push(queue, sensitiveData);
|
|
548
|
-
await sleep(100);
|
|
549
|
-
|
|
550
|
-
// Take and verify decryption
|
|
551
|
-
let decryptedCorrectly = false;
|
|
552
|
-
for await (const msg of client.take(queue, { limit: 1 })) {
|
|
553
|
-
// Check if data is properly decrypted
|
|
554
|
-
if (msg.data && typeof msg.data === 'object') {
|
|
555
|
-
if (msg.data.encrypted || msg.data.iv || msg.data.authTag) {
|
|
556
|
-
throw new Error('Consumer received encrypted data instead of decrypted!');
|
|
557
|
-
}
|
|
558
|
-
if (msg.data.creditCard === sensitiveData.creditCard &&
|
|
559
|
-
msg.data.ssn === sensitiveData.ssn) {
|
|
560
|
-
decryptedCorrectly = true;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
await client.ack(msg);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
if (!decryptedCorrectly) {
|
|
567
|
-
throw new Error('Decrypted data does not match original');
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
passTest('Consumer receives properly decrypted messages');
|
|
571
|
-
} catch (error) {
|
|
572
|
-
failTest(error);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Test: Enterprise Error Handling
|
|
578
|
-
*/
|
|
579
|
-
export async function testEnterpriseErrorHandling(client) {
|
|
580
|
-
startTest('Enterprise Error Handling', 'enterprise');
|
|
581
|
-
|
|
582
|
-
try {
|
|
583
|
-
let errorsCaught = 0;
|
|
584
|
-
|
|
585
|
-
// Test 1: Invalid retention seconds
|
|
586
|
-
try {
|
|
587
|
-
await client.queue('test-enterprise-errors', {
|
|
588
|
-
retentionEnabled: true,
|
|
589
|
-
retentionSeconds: -10 // Invalid negative value
|
|
590
|
-
});
|
|
591
|
-
errorsCaught++;
|
|
592
|
-
} catch (error) {
|
|
593
|
-
errorsCaught++;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Test 2: Configure encryption without key (if key not set)
|
|
597
|
-
if (!process.env.QUEEN_ENCRYPTION_KEY) {
|
|
598
|
-
try {
|
|
599
|
-
await client.queue('test-enterprise-no-key', {
|
|
600
|
-
encryptionEnabled: true
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// Try to push encrypted message without key
|
|
604
|
-
await client.push('test-enterprise-no-key', {
|
|
605
|
-
message: 'Should fail without key'
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
errorsCaught++;
|
|
609
|
-
} catch (error) {
|
|
610
|
-
errorsCaught++;
|
|
611
|
-
}
|
|
612
|
-
} else {
|
|
613
|
-
errorsCaught++; // Count as handled
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Test 3: Invalid max wait time
|
|
617
|
-
try {
|
|
618
|
-
await dbPool.query(`
|
|
619
|
-
INSERT INTO queen.queues (name, max_wait_time_seconds)
|
|
620
|
-
VALUES ('test-invalid-wait', -100)
|
|
621
|
-
ON CONFLICT (name) DO UPDATE SET max_wait_time_seconds = EXCLUDED.max_wait_time_seconds
|
|
622
|
-
`);
|
|
623
|
-
errorsCaught++;
|
|
624
|
-
} catch (error) {
|
|
625
|
-
errorsCaught++;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (errorsCaught >= 2) {
|
|
629
|
-
passTest(`Enterprise error handling works (${errorsCaught} scenarios handled)`);
|
|
630
|
-
} else {
|
|
631
|
-
throw new Error(`Only ${errorsCaught} error scenarios handled`);
|
|
632
|
-
}
|
|
633
|
-
} catch (error) {
|
|
634
|
-
failTest(error);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|