501 lines
16 KiB
TypeScript
501 lines
16 KiB
TypeScript
/**
|
|
* EventBridge Pipes integration tests.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import {
|
|
PipesClient,
|
|
CreatePipeCommand,
|
|
DescribePipeCommand,
|
|
ListPipesCommand,
|
|
UpdatePipeCommand,
|
|
DeletePipeCommand,
|
|
StartPipeCommand,
|
|
StopPipeCommand,
|
|
RequestedPipeState,
|
|
} from '@aws-sdk/client-pipes';
|
|
import {
|
|
SQSClient,
|
|
CreateQueueCommand,
|
|
DeleteQueueCommand,
|
|
SendMessageCommand,
|
|
ReceiveMessageCommand,
|
|
GetQueueUrlCommand,
|
|
GetQueueAttributesCommand,
|
|
} from '@aws-sdk/client-sqs';
|
|
import { makeClient, uniqueName, ACCOUNT, REGION, sleep } from './setup';
|
|
|
|
const ROLE_ARN = `arn:aws:iam::${ACCOUNT}:role/pipe-role`;
|
|
|
|
const sqsArn = (queueName: string) =>
|
|
`arn:aws:sqs:${REGION}:${ACCOUNT}:${queueName}`;
|
|
|
|
const getQueueUrl = async (sqs: SQSClient, name: string): Promise<string> => {
|
|
try {
|
|
const r = await sqs.send(new GetQueueUrlCommand({ QueueName: name }));
|
|
return r.QueueUrl ?? '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
};
|
|
|
|
describe('Pipes CRUD', () => {
|
|
let pipes: PipesClient;
|
|
let sqs: SQSClient;
|
|
let pipeName: string;
|
|
let srcQueue: string;
|
|
let tgtQueue: string;
|
|
|
|
beforeAll(() => {
|
|
pipes = makeClient(PipesClient);
|
|
sqs = makeClient(SQSClient);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Cleanup is handled per-test
|
|
});
|
|
|
|
it('should create a pipe in STOPPED state', async () => {
|
|
pipeName = `node-pipe-create-${uniqueName()}`;
|
|
srcQueue = `node-pipe-src-create-${uniqueName()}`;
|
|
tgtQueue = `node-pipe-tgt-create-${uniqueName()}`;
|
|
|
|
await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
|
|
try {
|
|
const response = await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.STOPPED,
|
|
})
|
|
);
|
|
expect(response.CurrentState).toBe('STOPPED');
|
|
expect(response.Arn).toContain(pipeName);
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, srcQueue) })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, tgtQueue) })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('should describe a pipe', async () => {
|
|
pipeName = `node-pipe-describe-${uniqueName()}`;
|
|
srcQueue = `node-pipe-src-describe-${uniqueName()}`;
|
|
tgtQueue = `node-pipe-tgt-describe-${uniqueName()}`;
|
|
|
|
await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.STOPPED,
|
|
})
|
|
);
|
|
|
|
const response = await pipes.send(new DescribePipeCommand({ Name: pipeName }));
|
|
expect(response.Name).toBe(pipeName);
|
|
expect(response.Source).toBe(sqsArn(srcQueue));
|
|
expect(response.Target).toBe(sqsArn(tgtQueue));
|
|
expect(response.CurrentState).toBe('STOPPED');
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, srcQueue) })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, tgtQueue) })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('should list pipes', async () => {
|
|
pipeName = `node-pipe-list-${uniqueName()}`;
|
|
srcQueue = `node-pipe-src-list-${uniqueName()}`;
|
|
tgtQueue = `node-pipe-tgt-list-${uniqueName()}`;
|
|
|
|
await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.STOPPED,
|
|
})
|
|
);
|
|
|
|
const response = await pipes.send(new ListPipesCommand({}));
|
|
const names = response.Pipes?.map((p) => p.Name) ?? [];
|
|
expect(names).toContain(pipeName);
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, srcQueue) })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, tgtQueue) })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('should update a pipe', async () => {
|
|
pipeName = `node-pipe-update-${uniqueName()}`;
|
|
srcQueue = `node-pipe-src-update-${uniqueName()}`;
|
|
tgtQueue = `node-pipe-tgt-update-${uniqueName()}`;
|
|
|
|
await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.STOPPED,
|
|
})
|
|
);
|
|
|
|
await pipes.send(
|
|
new UpdatePipeCommand({
|
|
Name: pipeName,
|
|
RoleArn: ROLE_ARN,
|
|
Description: 'updated via SDK',
|
|
DesiredState: RequestedPipeState.STOPPED,
|
|
})
|
|
);
|
|
|
|
const response = await pipes.send(new DescribePipeCommand({ Name: pipeName }));
|
|
expect(response.Description).toBe('updated via SDK');
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, srcQueue) })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, tgtQueue) })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('should delete a pipe', async () => {
|
|
pipeName = `node-pipe-delete-${uniqueName()}`;
|
|
srcQueue = `node-pipe-src-delete-${uniqueName()}`;
|
|
tgtQueue = `node-pipe-tgt-delete-${uniqueName()}`;
|
|
|
|
await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.STOPPED,
|
|
})
|
|
);
|
|
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName }));
|
|
|
|
await expect(
|
|
pipes.send(new DescribePipeCommand({ Name: pipeName }))
|
|
).rejects.toThrow();
|
|
} finally {
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, srcQueue) })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, tgtQueue) })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('should return error for non-existent pipe', async () => {
|
|
await expect(
|
|
pipes.send(new DescribePipeCommand({ Name: 'nonexistent-pipe' }))
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Pipes Lifecycle', () => {
|
|
let pipes: PipesClient;
|
|
let sqs: SQSClient;
|
|
|
|
beforeAll(() => {
|
|
pipes = makeClient(PipesClient);
|
|
sqs = makeClient(SQSClient);
|
|
});
|
|
|
|
it('should start and stop a pipe', async () => {
|
|
const pipeName = `node-pipe-lifecycle-${uniqueName()}`;
|
|
const srcQueue = `node-pipe-src-lifecycle-${uniqueName()}`;
|
|
const tgtQueue = `node-pipe-tgt-lifecycle-${uniqueName()}`;
|
|
|
|
await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.STOPPED,
|
|
})
|
|
);
|
|
|
|
const startResponse = await pipes.send(new StartPipeCommand({ Name: pipeName }));
|
|
expect(startResponse.CurrentState).toBe('RUNNING');
|
|
|
|
const stopResponse = await pipes.send(new StopPipeCommand({ Name: pipeName }));
|
|
expect(stopResponse.CurrentState).toBe('STOPPED');
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, srcQueue) })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: await getQueueUrl(sqs, tgtQueue) })).catch(() => {});
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Pipes Polling', () => {
|
|
let pipes: PipesClient;
|
|
let sqs: SQSClient;
|
|
|
|
beforeAll(() => {
|
|
pipes = makeClient(PipesClient);
|
|
sqs = makeClient(SQSClient);
|
|
});
|
|
|
|
it('should forward SQS messages to SQS target', async () => {
|
|
const pipeName = `node-pipe-fwd-${uniqueName()}`;
|
|
const srcQueue = `node-pipe-src-fwd-${uniqueName()}`;
|
|
const tgtQueue = `node-pipe-tgt-fwd-${uniqueName()}`;
|
|
|
|
const srcResp = await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
const srcUrl = srcResp.QueueUrl!;
|
|
const tgtResp = await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
const tgtUrl = tgtResp.QueueUrl!;
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.RUNNING,
|
|
})
|
|
);
|
|
|
|
await sqs.send(
|
|
new SendMessageCommand({ QueueUrl: srcUrl, MessageBody: 'hello from pipes' })
|
|
);
|
|
|
|
let found = false;
|
|
for (let i = 0; i < 15; i++) {
|
|
await sleep(1000);
|
|
const r = await sqs.send(
|
|
new ReceiveMessageCommand({
|
|
QueueUrl: tgtUrl,
|
|
MaxNumberOfMessages: 1,
|
|
WaitTimeSeconds: 1,
|
|
})
|
|
);
|
|
if (r.Messages?.length && r.Messages[0].Body?.includes('hello from pipes')) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(found).toBe(true);
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: srcUrl })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: tgtUrl })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('should filter messages with FilterCriteria', async () => {
|
|
const pipeName = `node-pipe-filter-${uniqueName()}`;
|
|
const srcQueue = `node-pipe-src-filter-${uniqueName()}`;
|
|
const tgtQueue = `node-pipe-tgt-filter-${uniqueName()}`;
|
|
|
|
const srcResp = await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
const srcUrl = srcResp.QueueUrl!;
|
|
const tgtResp = await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
const tgtUrl = tgtResp.QueueUrl!;
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.RUNNING,
|
|
SourceParameters: {
|
|
FilterCriteria: {
|
|
Filters: [
|
|
{ Pattern: '{"body": {"status": ["active"]}}' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
await sqs.send(
|
|
new SendMessageCommand({
|
|
QueueUrl: srcUrl,
|
|
MessageBody: JSON.stringify({ status: 'active', id: 'match-1' }),
|
|
})
|
|
);
|
|
await sqs.send(
|
|
new SendMessageCommand({
|
|
QueueUrl: srcUrl,
|
|
MessageBody: JSON.stringify({ status: 'inactive', id: 'no-match' }),
|
|
})
|
|
);
|
|
|
|
let found = false;
|
|
for (let i = 0; i < 15; i++) {
|
|
await sleep(1000);
|
|
const r = await sqs.send(
|
|
new ReceiveMessageCommand({
|
|
QueueUrl: tgtUrl,
|
|
MaxNumberOfMessages: 10,
|
|
WaitTimeSeconds: 1,
|
|
})
|
|
);
|
|
if (r.Messages?.some((m) => m.Body?.includes('match-1'))) {
|
|
const hasNonMatch = r.Messages.some((m) => m.Body?.includes('no-match'));
|
|
expect(hasNonMatch).toBe(false);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
expect(found).toBe(true);
|
|
|
|
// Source queue should be drained (non-matching messages deleted per AWS behavior)
|
|
const attrs = await sqs.send(
|
|
new GetQueueAttributesCommand({
|
|
QueueUrl: srcUrl,
|
|
AttributeNames: ['ApproximateNumberOfMessages'],
|
|
})
|
|
);
|
|
expect(attrs.Attributes?.ApproximateNumberOfMessages).toBe('0');
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: srcUrl })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: tgtUrl })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('should respect BatchSize in SourceParameters', async () => {
|
|
const pipeName = `node-pipe-batch-${uniqueName()}`;
|
|
const srcQueue = `node-pipe-src-batch-${uniqueName()}`;
|
|
const tgtQueue = `node-pipe-tgt-batch-${uniqueName()}`;
|
|
|
|
const srcResp = await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
const srcUrl = srcResp.QueueUrl!;
|
|
const tgtResp = await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
const tgtUrl = tgtResp.QueueUrl!;
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.RUNNING,
|
|
SourceParameters: {
|
|
SqsQueueParameters: {
|
|
BatchSize: 1,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
for (let i = 1; i <= 3; i++) {
|
|
await sqs.send(
|
|
new SendMessageCommand({ QueueUrl: srcUrl, MessageBody: `batch-msg-${i}` })
|
|
);
|
|
}
|
|
|
|
const foundMessages = new Set<string>();
|
|
for (let i = 0; i < 20 && foundMessages.size < 3; i++) {
|
|
await sleep(1000);
|
|
const r = await sqs.send(
|
|
new ReceiveMessageCommand({
|
|
QueueUrl: tgtUrl,
|
|
MaxNumberOfMessages: 10,
|
|
WaitTimeSeconds: 1,
|
|
})
|
|
);
|
|
for (const msg of r.Messages ?? []) {
|
|
for (let j = 1; j <= 3; j++) {
|
|
if (msg.Body?.includes(`batch-msg-${j}`)) {
|
|
foundMessages.add(`batch-msg-${j}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
expect(foundMessages.size).toBe(3);
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: srcUrl })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: tgtUrl })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('should not forward messages when pipe is stopped', async () => {
|
|
const pipeName = `node-pipe-nofwd-${uniqueName()}`;
|
|
const srcQueue = `node-pipe-src-nofwd-${uniqueName()}`;
|
|
const tgtQueue = `node-pipe-tgt-nofwd-${uniqueName()}`;
|
|
|
|
const srcResp = await sqs.send(new CreateQueueCommand({ QueueName: srcQueue }));
|
|
const srcUrl = srcResp.QueueUrl!;
|
|
const tgtResp = await sqs.send(new CreateQueueCommand({ QueueName: tgtQueue }));
|
|
const tgtUrl = tgtResp.QueueUrl!;
|
|
|
|
try {
|
|
await pipes.send(
|
|
new CreatePipeCommand({
|
|
Name: pipeName,
|
|
Source: sqsArn(srcQueue),
|
|
Target: sqsArn(tgtQueue),
|
|
RoleArn: ROLE_ARN,
|
|
DesiredState: RequestedPipeState.STOPPED,
|
|
})
|
|
);
|
|
|
|
await sqs.send(
|
|
new SendMessageCommand({ QueueUrl: srcUrl, MessageBody: 'should not forward' })
|
|
);
|
|
|
|
await sleep(3000);
|
|
|
|
const attrs = await sqs.send(
|
|
new GetQueueAttributesCommand({
|
|
QueueUrl: srcUrl,
|
|
AttributeNames: ['ApproximateNumberOfMessages'],
|
|
})
|
|
);
|
|
expect(attrs.Attributes?.ApproximateNumberOfMessages).toBe('1');
|
|
|
|
const r = await sqs.send(
|
|
new ReceiveMessageCommand({
|
|
QueueUrl: tgtUrl,
|
|
MaxNumberOfMessages: 1,
|
|
WaitTimeSeconds: 1,
|
|
})
|
|
);
|
|
expect(r.Messages ?? []).toHaveLength(0);
|
|
} finally {
|
|
await pipes.send(new DeletePipeCommand({ Name: pipeName })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: srcUrl })).catch(() => {});
|
|
await sqs.send(new DeleteQueueCommand({ QueueUrl: tgtUrl })).catch(() => {});
|
|
}
|
|
});
|
|
});
|