Skip to content

Commit 2dc22e4

Browse files
committed
sqs integration testin
1 parent c8b2b55 commit 2dc22e4

2 files changed

Lines changed: 300 additions & 0 deletions

File tree

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
2+
import { GenericContainer, type StartedTestContainer } from 'testcontainers';
3+
import { SQSClient, CreateQueueCommand, DeleteQueueCommand, PurgeQueueCommand } from '@aws-sdk/client-sqs';
4+
import { SqsQueue } from '../../src/drivers/sqs.ts';
5+
import { SQSClientV3Adapter } from '../utils/sqs-v3-adapter.ts';
6+
7+
interface TestJobs {
8+
'process-data': { id: number; data: string };
9+
'send-notification': { email: string; message: string };
10+
}
11+
12+
describe('SQS Integration Tests (LocalStack)', () => {
13+
let container: StartedTestContainer;
14+
let sqsClient: SQSClient;
15+
let queueUrl: string;
16+
let queue: SqsQueue<TestJobs>;
17+
18+
beforeAll(async () => {
19+
// Start LocalStack container
20+
console.log('Starting LocalStack container...');
21+
container = await new GenericContainer('localstack/localstack:3.0')
22+
.withEnvironment({
23+
'SERVICES': 'sqs',
24+
'DEBUG': '1',
25+
'PERSISTENCE': '0'
26+
})
27+
.withExposedPorts(4566)
28+
.withStartupTimeout(90000)
29+
.start();
30+
31+
const endpoint = `http://${container.getHost()}:${container.getMappedPort(4566)}`;
32+
console.log(`LocalStack started at ${endpoint}`);
33+
34+
// Create SQS client pointing to LocalStack
35+
sqsClient = new SQSClient({
36+
region: 'us-east-1',
37+
endpoint,
38+
credentials: {
39+
accessKeyId: 'test',
40+
secretAccessKey: 'test'
41+
}
42+
});
43+
44+
// Create test queue
45+
const createQueueResult = await sqsClient.send(new CreateQueueCommand({
46+
QueueName: 'test-queue'
47+
}));
48+
49+
queueUrl = createQueueResult.QueueUrl!;
50+
console.log(`Test queue created: ${queueUrl}`);
51+
}, 120000); // 2 minute timeout for container startup
52+
53+
afterAll(async () => {
54+
// Clean up
55+
if (sqsClient && queueUrl) {
56+
try {
57+
await sqsClient.send(new DeleteQueueCommand({ QueueUrl: queueUrl }));
58+
} catch (error) {
59+
console.warn('Failed to delete test queue:', error);
60+
}
61+
}
62+
63+
if (container) {
64+
await container.stop();
65+
}
66+
});
67+
68+
beforeEach(async () => {
69+
// Purge queue before each test
70+
try {
71+
await sqsClient.send(new PurgeQueueCommand({ QueueUrl: queueUrl }));
72+
} catch (error) {
73+
// Queue might be empty, ignore
74+
}
75+
76+
// Create fresh queue instance using the v3 adapter
77+
const adapter = new SQSClientV3Adapter(sqsClient);
78+
queue = new SqsQueue<TestJobs>(adapter, queueUrl);
79+
});
80+
81+
describe('Real SQS Operations', () => {
82+
it('should add a job and retrieve it from real SQS', async () => {
83+
const jobId = await queue.addJob('process-data', {
84+
payload: { id: 1, data: 'test data' }
85+
});
86+
87+
expect(jobId).toBeTruthy();
88+
89+
// Reserve the job
90+
const reserved = await queue['reserve'](1);
91+
expect(reserved).not.toBeNull();
92+
expect(reserved!.id).toBe(jobId);
93+
94+
// Parse the payload
95+
const payload = JSON.parse(reserved!.payload);
96+
expect(payload).toEqual({
97+
name: 'process-data',
98+
payload: { id: 1, data: 'test data' }
99+
});
100+
});
101+
102+
it('should handle job execution lifecycle', async () => {
103+
const processedJobs: Array<{ id: number; data: string }> = [];
104+
105+
// Register job handler
106+
queue.onJob('process-data', async (payload) => {
107+
processedJobs.push(payload);
108+
return `Processed: ${payload.data}`;
109+
});
110+
111+
// Add job
112+
await queue.addJob('process-data', {
113+
payload: { id: 2, data: 'lifecycle test' }
114+
});
115+
116+
// Process job
117+
await queue.run(false); // Run once
118+
119+
expect(processedJobs).toHaveLength(1);
120+
expect(processedJobs[0]).toEqual({ id: 2, data: 'lifecycle test' });
121+
});
122+
123+
it('should respect delay seconds', async () => {
124+
const startTime = Date.now();
125+
126+
await queue.addJob('process-data', {
127+
payload: { id: 3, data: 'delayed job' },
128+
delay: 2 // 2 seconds delay
129+
});
130+
131+
// Immediate attempt should return null (job not available yet)
132+
const immediateReserve = await queue['reserve'](0);
133+
expect(immediateReserve).toBeNull();
134+
135+
// Wait for delay + small buffer
136+
await new Promise(resolve => setTimeout(resolve, 3000));
137+
138+
const delayedReserve = await queue['reserve'](1);
139+
expect(delayedReserve).not.toBeNull();
140+
141+
const elapsedTime = Date.now() - startTime;
142+
expect(elapsedTime).toBeGreaterThanOrEqual(2000); // At least 2 seconds
143+
});
144+
145+
it('should handle TTR (Time To Run) correctly', async () => {
146+
await queue.addJob('process-data', {
147+
payload: { id: 4, data: 'ttr test' },
148+
ttr: 5 // 5 seconds TTR
149+
});
150+
151+
// Reserve job
152+
const reserved = await queue['reserve'](1);
153+
expect(reserved).not.toBeNull();
154+
expect(reserved!.meta.ttr).toBe(5);
155+
156+
// Verify job metadata contains receiptHandle for SQS
157+
expect(reserved!.meta.receiptHandle).toBeTruthy();
158+
});
159+
160+
it('should handle multiple jobs concurrently', async () => {
161+
const promises = [];
162+
163+
// Add multiple jobs
164+
for (let i = 0; i < 5; i++) {
165+
promises.push(
166+
queue.addJob('process-data', {
167+
payload: { id: i, data: `concurrent job ${i}` }
168+
})
169+
);
170+
}
171+
172+
const jobIds = await Promise.all(promises);
173+
expect(jobIds).toHaveLength(5);
174+
expect(new Set(jobIds).size).toBe(5); // All IDs should be unique
175+
176+
// Process all jobs
177+
const processedJobs: Array<{ id: number; data: string }> = [];
178+
queue.onJob('process-data', async (payload) => {
179+
processedJobs.push(payload);
180+
});
181+
182+
// Run queue multiple times to process all jobs
183+
for (let i = 0; i < 5; i++) {
184+
await queue.run(false);
185+
}
186+
187+
expect(processedJobs).toHaveLength(5);
188+
});
189+
190+
it('should handle job errors and event system', async () => {
191+
let errorOccurred = false;
192+
let beforeExecCalled = false;
193+
let afterErrorCalled = false;
194+
195+
// Register event handlers
196+
queue.on('beforeExec', () => {
197+
beforeExecCalled = true;
198+
});
199+
200+
queue.on('afterError', (event) => {
201+
afterErrorCalled = true;
202+
expect(event.error).toBeInstanceOf(Error);
203+
});
204+
205+
// Register failing job handler
206+
queue.onJob('process-data', async () => {
207+
throw new Error('Intentional test failure');
208+
});
209+
210+
await queue.addJob('process-data', {
211+
payload: { id: 5, data: 'error test' }
212+
});
213+
214+
// Process the failing job
215+
await queue.run(false);
216+
217+
expect(beforeExecCalled).toBe(true);
218+
expect(afterErrorCalled).toBe(true);
219+
});
220+
221+
it('should support different job types', async () => {
222+
const processedData: any[] = [];
223+
const processedNotifications: any[] = [];
224+
225+
queue.onJob('process-data', async (payload) => {
226+
processedData.push(payload);
227+
});
228+
229+
queue.onJob('send-notification', async (payload) => {
230+
processedNotifications.push(payload);
231+
});
232+
233+
await queue.addJob('process-data', {
234+
payload: { id: 6, data: 'data job' }
235+
});
236+
237+
await queue.addJob('send-notification', {
238+
payload: { email: 'test@example.com', message: 'Hello' }
239+
});
240+
241+
// Process both jobs
242+
await queue.run(false);
243+
await queue.run(false);
244+
245+
expect(processedData).toHaveLength(1);
246+
expect(processedData[0]).toEqual({ id: 6, data: 'data job' });
247+
248+
expect(processedNotifications).toHaveLength(1);
249+
expect(processedNotifications[0]).toEqual({
250+
email: 'test@example.com',
251+
message: 'Hello'
252+
});
253+
});
254+
255+
it('should handle SQS status limitation gracefully', async () => {
256+
const jobId = await queue.addJob('process-data', {
257+
payload: { id: 7, data: 'status test' }
258+
});
259+
260+
// SQS doesn't support job status tracking, so this should throw
261+
await expect(queue.status(jobId)).rejects.toThrow('SQS does not support status');
262+
});
263+
});
264+
});

tests/utils/sqs-v3-adapter.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
SQSClient,
3+
SendMessageCommand,
4+
ReceiveMessageCommand,
5+
DeleteMessageCommand,
6+
ChangeMessageVisibilityCommand,
7+
type SendMessageCommandInput,
8+
type ReceiveMessageCommandInput,
9+
type DeleteMessageCommandInput,
10+
type ChangeMessageVisibilityCommandInput
11+
} from '@aws-sdk/client-sqs';
12+
import type { SimplifiedSQSClient } from '../../src/drivers/sqs.ts';
13+
14+
/**
15+
* Adapter that wraps AWS SDK v3 SQSClient to match the v2 interface
16+
* expected by the SqsQueue driver
17+
*/
18+
export class SQSClientV3Adapter implements SimplifiedSQSClient {
19+
constructor(private client: SQSClient) {}
20+
21+
async sendMessage(params: SendMessageCommandInput) {
22+
return this.client.send(new SendMessageCommand(params));
23+
}
24+
25+
async receiveMessage(params: ReceiveMessageCommandInput) {
26+
return this.client.send(new ReceiveMessageCommand(params));
27+
}
28+
29+
async deleteMessage(params: DeleteMessageCommandInput) {
30+
return this.client.send(new DeleteMessageCommand(params));
31+
}
32+
33+
async changeMessageVisibility(params: ChangeMessageVisibilityCommandInput) {
34+
return this.client.send(new ChangeMessageVisibilityCommand(params));
35+
}
36+
}

0 commit comments

Comments
 (0)