@remit/sqs-client 0.0.15 → 0.0.17
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/dist/attempt-budget.d.ts +14 -0
- package/{src/attempt-budget.ts → dist/attempt-budget.js} +8 -10
- package/{src/heartbeat.ts → dist/heartbeat.d.ts} +2 -28
- package/dist/heartbeat.js +35 -0
- package/dist/index.d.ts +5 -0
- package/{src/index.ts → dist/index.js} +7 -16
- package/dist/poller.d.ts +54 -0
- package/dist/poller.js +180 -0
- package/{src/producer.ts → dist/producer.d.ts} +23 -39
- package/dist/producer.js +105 -0
- package/package.json +52 -46
- package/src/attempt-budget.test.ts +0 -58
- package/src/heartbeat.test.ts +0 -102
- package/src/index.test.ts +0 -39
- package/src/poller.test.ts +0 -301
- package/src/poller.ts +0 -283
- package/src/producer.test.ts +0 -150
- package/tsconfig.json +0 -8
package/src/poller.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { inspect } from "node:util";
|
|
3
|
-
import {
|
|
4
|
-
DeleteMessageCommand,
|
|
5
|
-
ReceiveMessageCommand,
|
|
6
|
-
type ReceiveMessageCommandOutput,
|
|
7
|
-
} from "@aws-sdk/client-sqs";
|
|
8
|
-
import type {
|
|
9
|
-
Context,
|
|
10
|
-
SQSBatchResponse,
|
|
11
|
-
SQSEvent,
|
|
12
|
-
SQSHandler,
|
|
13
|
-
} from "aws-lambda";
|
|
14
|
-
import {
|
|
15
|
-
clearHeartbeats,
|
|
16
|
-
createHeartbeat,
|
|
17
|
-
type Heartbeat,
|
|
18
|
-
} from "./heartbeat.js";
|
|
19
|
-
import { createQueueProducer } from "./producer.js";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Production queue poller — the non-e2e counterpart of the per-worker
|
|
23
|
-
* `e2e-processor-shim.ts` scripts. It receives from an SQS-compatible
|
|
24
|
-
* endpoint (real SQS or ElasticMQ), wraps each batch in an `SQSEvent`, and
|
|
25
|
-
* invokes the exact production Lambda `handler`, honouring
|
|
26
|
-
* `batchItemFailures` the way the SQS service does: delete what succeeded,
|
|
27
|
-
* leave failures for redelivery.
|
|
28
|
-
*
|
|
29
|
-
* Unlike the e2e shim (one child process per queue, via `node:cluster`),
|
|
30
|
-
* this runs every target queue as a concurrent poll loop inside a single
|
|
31
|
-
* process — the right shape for a container whose only job is "poll these
|
|
32
|
-
* queues, invoke this handler, exit on SIGTERM."
|
|
33
|
-
*/
|
|
34
|
-
export interface QueuePollerLog {
|
|
35
|
-
info: (fields: Record<string, unknown>, message: string) => void;
|
|
36
|
-
error: (fields: Record<string, unknown>, message: string) => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface QueuePollerTarget {
|
|
40
|
-
readonly queueUrl: string;
|
|
41
|
-
readonly handler: SQSHandler;
|
|
42
|
-
/** Reported as the Lambda `Context.functionName` the handler observes. */
|
|
43
|
-
readonly functionName: string;
|
|
44
|
-
readonly maxMessages?: number;
|
|
45
|
-
readonly visibilityTimeoutSeconds?: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface RunQueuePollerOptions {
|
|
49
|
-
readonly targets: readonly QueuePollerTarget[];
|
|
50
|
-
readonly log: QueuePollerLog;
|
|
51
|
-
readonly signals?: readonly NodeJS.Signals[];
|
|
52
|
-
/** One per target. Defaults to a file under `WORKER_HEARTBEAT_PREFIX`. */
|
|
53
|
-
readonly createHeartbeat?: (name: string) => Heartbeat;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// One message per receive, not the SQS maximum of 10. Handlers process a batch
|
|
57
|
-
// sequentially, so a batch of ten is ten handler runs between two heartbeats:
|
|
58
|
-
// ten stalled SMTP sends at nodemailer's 300 s socket timeout is 50 minutes of
|
|
59
|
-
// legitimate silence, and a threshold that tolerates it detects nothing. At one
|
|
60
|
-
// message the quiet stretch a healthy loop can produce is one handler run, and
|
|
61
|
-
// the extra receive per message is a millisecond against the local queue
|
|
62
|
-
// sidecar — the same total work either way.
|
|
63
|
-
const DEFAULT_MAX_MESSAGES = 1;
|
|
64
|
-
/**
|
|
65
|
-
* What a handler has before the queue redelivers the record underneath it.
|
|
66
|
-
* Exported because a handler that budgets its own wait has to agree with it;
|
|
67
|
-
* `deploy/vps/queues.json` sets the same number on the queue itself, and this
|
|
68
|
-
* is the one that applies wherever a target passes no override.
|
|
69
|
-
*/
|
|
70
|
-
export const DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
|
|
71
|
-
|
|
72
|
-
const pollTarget = async (
|
|
73
|
-
target: QueuePollerTarget,
|
|
74
|
-
log: QueuePollerLog,
|
|
75
|
-
isShuttingDown: () => boolean,
|
|
76
|
-
shutdownSignal: AbortSignal,
|
|
77
|
-
buildHeartbeat: (name: string) => Heartbeat,
|
|
78
|
-
): Promise<void> => {
|
|
79
|
-
const queueName = new URL(target.queueUrl).pathname.split("/").pop();
|
|
80
|
-
const heartbeat = buildHeartbeat(queueName ?? target.functionName);
|
|
81
|
-
const sqs = createQueueProducer({ queueUrl: target.queueUrl });
|
|
82
|
-
const maxMessages = target.maxMessages ?? DEFAULT_MAX_MESSAGES;
|
|
83
|
-
const visibilityTimeout =
|
|
84
|
-
target.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS;
|
|
85
|
-
log.info({ queue: queueName, maxMessages }, "poller: started");
|
|
86
|
-
|
|
87
|
-
// Always long-poll (SQS max WaitTimeSeconds). An empty long poll is free
|
|
88
|
-
// and returns as soon as a message arrives, so this is never slower than
|
|
89
|
-
// short-polling under load — it just stops burning a ReceiveMessage call
|
|
90
|
-
// (and, against real SQS, its cost) every loop iteration while traffic is
|
|
91
|
-
// continuous.
|
|
92
|
-
const LONG_POLL_WAIT_SECONDS = 20;
|
|
93
|
-
|
|
94
|
-
while (!isShuttingDown()) {
|
|
95
|
-
// Top of the receive attempt, before the long poll: this loop's own file,
|
|
96
|
-
// so a loop that stops turning — wedged in a socket read, or in a handler
|
|
97
|
-
// that never returns — goes stale on its own however busy its siblings are.
|
|
98
|
-
//
|
|
99
|
-
// A write that fails must not take the worker down with it. A full disk is
|
|
100
|
-
// the likeliest cause and the moment the loop should stay up, failing
|
|
101
|
-
// records into the dead-letter queues with logs rather than crash-looping
|
|
102
|
-
// on the monitoring. The missed beat is itself the signal: the healthcheck
|
|
103
|
-
// goes stale, which is what this file exists to report.
|
|
104
|
-
await heartbeat().catch((error) => {
|
|
105
|
-
log.error(
|
|
106
|
-
{ queue: queueName, error: inspect(error) },
|
|
107
|
-
"poller: heartbeat write failed",
|
|
108
|
-
);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
let response: ReceiveMessageCommandOutput;
|
|
112
|
-
try {
|
|
113
|
-
// Abort the receive on shutdown. Without this, a container stopped
|
|
114
|
-
// while idle waits out the current 20s long poll before the loop
|
|
115
|
-
// checks the flag again, and a self-update then burns the whole
|
|
116
|
-
// stop grace on every idle worker — the bulk of the visible restart
|
|
117
|
-
// downtime. The abort ends the in-flight receive at once; an
|
|
118
|
-
// in-flight handler below is never interrupted, so a message being
|
|
119
|
-
// processed still finishes cleanly. The heartbeat above already wrote
|
|
120
|
-
// this iteration, so the abort exits without a further beat.
|
|
121
|
-
response = await sqs.send(
|
|
122
|
-
new ReceiveMessageCommand({
|
|
123
|
-
QueueUrl: target.queueUrl,
|
|
124
|
-
MaxNumberOfMessages: maxMessages,
|
|
125
|
-
WaitTimeSeconds: LONG_POLL_WAIT_SECONDS,
|
|
126
|
-
VisibilityTimeout: visibilityTimeout,
|
|
127
|
-
MessageSystemAttributeNames: ["ApproximateReceiveCount"],
|
|
128
|
-
}),
|
|
129
|
-
{ abortSignal: shutdownSignal },
|
|
130
|
-
);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (shutdownSignal.aborted) break;
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!response.Messages || response.Messages.length === 0) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const messages = response.Messages.flatMap((m) =>
|
|
141
|
-
m.Body && m.ReceiptHandle && m.MessageId
|
|
142
|
-
? [
|
|
143
|
-
{
|
|
144
|
-
messageId: m.MessageId,
|
|
145
|
-
receiptHandle: m.ReceiptHandle,
|
|
146
|
-
body: m.Body,
|
|
147
|
-
receiveCount: m.Attributes?.ApproximateReceiveCount ?? "1",
|
|
148
|
-
},
|
|
149
|
-
]
|
|
150
|
-
: [],
|
|
151
|
-
);
|
|
152
|
-
if (messages.length === 0) continue;
|
|
153
|
-
|
|
154
|
-
// One batch is one handler invocation, so the id is minted per batch: it is
|
|
155
|
-
// the `requestId` every line of that invocation is grouped by (see
|
|
156
|
-
// deploy/vps/README.md, "Logs"). A context built once per target would put
|
|
157
|
-
// the whole queue's lifetime under one id, which correlates nothing.
|
|
158
|
-
//
|
|
159
|
-
// Minted before the poller's own bracketing lines rather than next to the
|
|
160
|
-
// handler call: grouping on the id has to reach the line saying whether the
|
|
161
|
-
// message was deleted or left for redelivery, which is what someone looks
|
|
162
|
-
// for after a failure.
|
|
163
|
-
const lambdaContext = {
|
|
164
|
-
functionName: target.functionName,
|
|
165
|
-
awsRequestId: randomUUID(),
|
|
166
|
-
} as Context;
|
|
167
|
-
|
|
168
|
-
log.info(
|
|
169
|
-
{
|
|
170
|
-
queue: queueName,
|
|
171
|
-
count: messages.length,
|
|
172
|
-
requestId: lambdaContext.awsRequestId,
|
|
173
|
-
},
|
|
174
|
-
"poller: invoking handler",
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const event: SQSEvent = {
|
|
178
|
-
Records: messages.map((m) => ({
|
|
179
|
-
messageId: m.messageId,
|
|
180
|
-
receiptHandle: m.receiptHandle,
|
|
181
|
-
body: m.body,
|
|
182
|
-
attributes: {
|
|
183
|
-
ApproximateReceiveCount: m.receiveCount,
|
|
184
|
-
SentTimestamp: "0",
|
|
185
|
-
SenderId: "poller",
|
|
186
|
-
ApproximateFirstReceiveTimestamp: "0",
|
|
187
|
-
},
|
|
188
|
-
messageAttributes: {},
|
|
189
|
-
md5OfBody: "",
|
|
190
|
-
eventSource: "aws:sqs",
|
|
191
|
-
eventSourceARN: target.queueUrl,
|
|
192
|
-
awsRegion: "local",
|
|
193
|
-
})),
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const result = (await target.handler(event, lambdaContext, () => {})) as
|
|
197
|
-
| SQSBatchResponse
|
|
198
|
-
| undefined;
|
|
199
|
-
|
|
200
|
-
const failedIds = new Set(
|
|
201
|
-
(result?.batchItemFailures ?? []).map((f) => f.itemIdentifier),
|
|
202
|
-
);
|
|
203
|
-
const succeeded = messages.filter((m) => !failedIds.has(m.messageId));
|
|
204
|
-
|
|
205
|
-
// Batch is capped at maxMessages (one by default, 10 is the SQS ceiling),
|
|
206
|
-
// so a handful of concurrent DeleteMessage calls is safe and avoids paying
|
|
207
|
-
// N sequential round trips between every receive/handler cycle.
|
|
208
|
-
await Promise.all(
|
|
209
|
-
succeeded.map((message) =>
|
|
210
|
-
sqs.send(
|
|
211
|
-
new DeleteMessageCommand({
|
|
212
|
-
QueueUrl: target.queueUrl,
|
|
213
|
-
ReceiptHandle: message.receiptHandle,
|
|
214
|
-
}),
|
|
215
|
-
),
|
|
216
|
-
),
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
log.info(
|
|
220
|
-
{
|
|
221
|
-
queue: queueName,
|
|
222
|
-
deleted: succeeded.length,
|
|
223
|
-
leftForRedelivery: failedIds.size,
|
|
224
|
-
requestId: lambdaContext.awsRequestId,
|
|
225
|
-
},
|
|
226
|
-
"poller: batch processed",
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
log.info({ queue: queueName }, "poller: stopped");
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* received (default: SIGINT, SIGTERM). The signal aborts each loop's in-flight
|
|
235
|
-
* long-poll receive so an idle worker returns at once instead of waiting out
|
|
236
|
-
* the current 20s poll; a loop mid-handler finishes that handler first. Rejects
|
|
237
|
-
* (crashes the process) if any loop throws — a stuck poller should exit loudly,
|
|
238
|
-
* not degrade silently.
|
|
239
|
-
*
|
|
240
|
-
* Every loop writes its own heartbeat file (see heartbeat.ts) at the top of its
|
|
241
|
-
* receive attempt, which is how a loop that hangs instead of throwing — the
|
|
242
|
-
* case this cannot make loud by itself — becomes visible.
|
|
243
|
-
*/
|
|
244
|
-
export const runQueuePoller = async (
|
|
245
|
-
options: RunQueuePollerOptions,
|
|
246
|
-
): Promise<void> => {
|
|
247
|
-
const { targets, log } = options;
|
|
248
|
-
const signals = options.signals ?? ["SIGINT", "SIGTERM"];
|
|
249
|
-
const buildHeartbeat = options.createHeartbeat ?? createHeartbeat;
|
|
250
|
-
|
|
251
|
-
if (targets.length === 0) {
|
|
252
|
-
throw new Error("runQueuePoller: no targets configured");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Files from the previous run of this service, before its loops write again:
|
|
256
|
-
// the healthcheck reads the oldest file it finds, so one left behind by a
|
|
257
|
-
// queue this version no longer polls would fail the check forever.
|
|
258
|
-
await clearHeartbeats().catch((error) => {
|
|
259
|
-
log.error({ error: inspect(error) }, "poller: heartbeat cleanup failed");
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
let shuttingDown = false;
|
|
263
|
-
const isShuttingDown = () => shuttingDown;
|
|
264
|
-
// Aborted alongside the flag so a loop currently blocked in its long-poll
|
|
265
|
-
// receive returns at once, rather than after up to WaitTimeSeconds.
|
|
266
|
-
const shutdown = new AbortController();
|
|
267
|
-
const onSignal = (signal: NodeJS.Signals) => {
|
|
268
|
-
log.info({ signal }, "poller: shutdown signal received");
|
|
269
|
-
shuttingDown = true;
|
|
270
|
-
shutdown.abort();
|
|
271
|
-
};
|
|
272
|
-
for (const signal of signals) {
|
|
273
|
-
process.on(signal, onSignal);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
log.info({ queues: targets.map((t) => t.queueUrl) }, "poller: starting");
|
|
277
|
-
|
|
278
|
-
await Promise.all(
|
|
279
|
-
targets.map((target) =>
|
|
280
|
-
pollTarget(target, log, isShuttingDown, shutdown.signal, buildHeartbeat),
|
|
281
|
-
),
|
|
282
|
-
);
|
|
283
|
-
};
|
package/src/producer.test.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
-
import { AwsQueryProtocol } from "@aws-sdk/core/protocols";
|
|
5
|
-
import { createQueueProducer, isLocalEndpoint } from "./producer.js";
|
|
6
|
-
|
|
7
|
-
// The queue URL a self-hosted compose stack hands its workers: a plain
|
|
8
|
-
// http:// URL addressing the queue container by name, never "localhost".
|
|
9
|
-
const composeQueueUrl = "http://queue:9324/000000000000/mailboxes";
|
|
10
|
-
const awsQueueUrl =
|
|
11
|
-
"https://sqs.eu-west-1.amazonaws.com/123456789012/remit-dev-mailboxes.fifo";
|
|
12
|
-
|
|
13
|
-
const usesQueryProtocol = (client: SQSClient): boolean =>
|
|
14
|
-
client.config.protocol instanceof AwsQueryProtocol;
|
|
15
|
-
|
|
16
|
-
const resolvedEndpoint = async (
|
|
17
|
-
client: SQSClient,
|
|
18
|
-
): Promise<string | undefined> => {
|
|
19
|
-
const { endpoint } = client.config;
|
|
20
|
-
if (!endpoint) return undefined;
|
|
21
|
-
const resolved = await endpoint();
|
|
22
|
-
return `${resolved.protocol}//${resolved.hostname}:${resolved.port}`;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
describe("isLocalEndpoint", () => {
|
|
26
|
-
it("treats a non-localhost http:// queue URL as local", () => {
|
|
27
|
-
assert.equal(isLocalEndpoint(composeQueueUrl), true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("treats http://localhost as local", () => {
|
|
31
|
-
assert.equal(
|
|
32
|
-
isLocalEndpoint("http://localhost:9324/000000000000/mailboxes"),
|
|
33
|
-
true,
|
|
34
|
-
);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("treats https://localhost as local", () => {
|
|
38
|
-
assert.equal(
|
|
39
|
-
isLocalEndpoint("https://localhost:9324/000000000000/mailboxes"),
|
|
40
|
-
true,
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("treats a real SQS queue URL as remote", () => {
|
|
45
|
-
assert.equal(isLocalEndpoint(awsQueueUrl), false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// A prefix match on "https://localhost" reads all of these as local. The
|
|
49
|
-
// host is localhost in none of them.
|
|
50
|
-
it("treats an https host that merely starts with localhost as remote", () => {
|
|
51
|
-
assert.equal(isLocalEndpoint("https://localhost.example.com/0/q"), false);
|
|
52
|
-
assert.equal(
|
|
53
|
-
isLocalEndpoint("https://localhost-queue.example.com/0/q"),
|
|
54
|
-
false,
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("treats an https URL with localhost in the userinfo as remote", () => {
|
|
59
|
-
assert.equal(isLocalEndpoint("https://localhost@example.com/0/q"), false);
|
|
60
|
-
assert.equal(
|
|
61
|
-
isLocalEndpoint("https://localhost:pass@example.com/0/q"),
|
|
62
|
-
false,
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// The mirror image: a prefix match misses these, and they are local.
|
|
67
|
-
it("treats a loopback https URL carrying credentials as local", () => {
|
|
68
|
-
assert.equal(isLocalEndpoint("https://user:pass@localhost/0/q"), true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("ignores scheme case", () => {
|
|
72
|
-
assert.equal(isLocalEndpoint("HTTP://queue:9324/0/q"), true);
|
|
73
|
-
assert.equal(isLocalEndpoint("HtTpS://localhost:9324/0/q"), true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("treats loopback addresses over https as local", () => {
|
|
77
|
-
assert.equal(isLocalEndpoint("https://127.0.0.1:9324/0/q"), true);
|
|
78
|
-
assert.equal(isLocalEndpoint("https://[::1]:9324/0/q"), true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("treats any http URL as local regardless of host, port, or credentials", () => {
|
|
82
|
-
assert.equal(isLocalEndpoint("http://queue/0/q"), true);
|
|
83
|
-
assert.equal(isLocalEndpoint("http://user:pass@queue:9324/0/q"), true);
|
|
84
|
-
assert.equal(isLocalEndpoint("http://example.com/0/q"), true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Must not throw: several callers construct their client at module load, so
|
|
88
|
-
// a throw here would take the worker down at import.
|
|
89
|
-
it("treats an unparseable queue URL as remote without throwing", () => {
|
|
90
|
-
assert.equal(isLocalEndpoint(""), false);
|
|
91
|
-
assert.equal(isLocalEndpoint("not a url"), false);
|
|
92
|
-
assert.equal(isLocalEndpoint("queue:9324/0/q"), false);
|
|
93
|
-
assert.equal(isLocalEndpoint("//queue:9324/0/q"), false);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("createQueueProducer", () => {
|
|
98
|
-
// Regression (self-host stack): a queue URL of http://queue:9324/... used to
|
|
99
|
-
// fall through to the AWS JSON protocol, so every send hit the local queue
|
|
100
|
-
// server with a body it cannot parse and mail never synced.
|
|
101
|
-
it("selects the query protocol for a non-localhost http:// queue URL", async () => {
|
|
102
|
-
const client = createQueueProducer({ queueUrl: composeQueueUrl });
|
|
103
|
-
assert.equal(usesQueryProtocol(client), true);
|
|
104
|
-
assert.equal(await resolvedEndpoint(client), "http://queue:9324");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("selects the query protocol for a localhost queue URL", () => {
|
|
108
|
-
const client = createQueueProducer({
|
|
109
|
-
queueUrl: "http://localhost:9324/000000000000/mailboxes",
|
|
110
|
-
});
|
|
111
|
-
assert.equal(usesQueryProtocol(client), true);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("leaves the default protocol and endpoint for a real AWS queue URL", async () => {
|
|
115
|
-
const client = createQueueProducer({ queueUrl: awsQueueUrl });
|
|
116
|
-
assert.equal(usesQueryProtocol(client), false);
|
|
117
|
-
assert.equal(await resolvedEndpoint(client), undefined);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("does not throw when the queue URL is unparseable", async () => {
|
|
121
|
-
const client = createQueueProducer({ queueUrl: "not a url" });
|
|
122
|
-
assert.equal(usesQueryProtocol(client), false);
|
|
123
|
-
assert.equal(await resolvedEndpoint(client), undefined);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("accepts an explicit endpoint override", async () => {
|
|
127
|
-
const client = createQueueProducer({
|
|
128
|
-
queueUrl: composeQueueUrl,
|
|
129
|
-
endpoint: "http://custom-endpoint:1234",
|
|
130
|
-
});
|
|
131
|
-
assert.equal(usesQueryProtocol(client), true);
|
|
132
|
-
assert.equal(await resolvedEndpoint(client), "http://custom-endpoint:1234");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("uses localCredentials only for a local endpoint", async () => {
|
|
136
|
-
const local = createQueueProducer({
|
|
137
|
-
queueUrl: composeQueueUrl,
|
|
138
|
-
localCredentials: { accessKeyId: "local", secretAccessKey: "local" },
|
|
139
|
-
env: {},
|
|
140
|
-
});
|
|
141
|
-
assert.equal((await local.config.credentials()).accessKeyId, "local");
|
|
142
|
-
|
|
143
|
-
const remote = createQueueProducer({
|
|
144
|
-
queueUrl: awsQueueUrl,
|
|
145
|
-
localCredentials: { accessKeyId: "local", secretAccessKey: "local" },
|
|
146
|
-
env: { SQS_ACCESS_KEY_ID: "real", SQS_SECRET_ACCESS_KEY: "real" },
|
|
147
|
-
});
|
|
148
|
-
assert.equal((await remote.config.credentials()).accessKeyId, "real");
|
|
149
|
-
});
|
|
150
|
-
});
|