@blokjs/trigger-pubsub 0.6.18 → 0.6.20

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.
@@ -1,269 +0,0 @@
1
- import type { PubSubMessage } from "@blokjs/runner";
2
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
- import { KafkaPubSubAdapter } from "../../src/adapters/KafkaPubSubAdapter";
4
-
5
- /**
6
- * Narrow shapes for the bits of `kafkajs` we touch in this test. The
7
- * runtime types in the package are loosely-typed (the adapter itself
8
- * pins `any` behind biome-ignore comments); these test-local interfaces
9
- * keep us on the safe `as unknown as <T>` boundary-cast path required
10
- * by the repo's no-`any`-in-tests rule.
11
- */
12
- interface KafkaAdminClient {
13
- connect(): Promise<void>;
14
- disconnect(): Promise<void>;
15
- createTopics(opts: {
16
- waitForLeaders?: boolean;
17
- topics: Array<{ topic: string; numPartitions: number; replicationFactor: number }>;
18
- }): Promise<boolean>;
19
- fetchTopicMetadata(opts: { topics: string[] }): Promise<unknown>;
20
- }
21
-
22
- interface KafkaConsumerForWarmup {
23
- connect(): Promise<void>;
24
- subscribe(opts: { topic: string; fromBeginning?: boolean }): Promise<void>;
25
- run(opts: { eachMessage: () => Promise<void> }): Promise<void>;
26
- disconnect(): Promise<void>;
27
- }
28
-
29
- interface KafkaClient {
30
- admin(): KafkaAdminClient;
31
- consumer(opts: { groupId: string }): KafkaConsumerForWarmup;
32
- }
33
-
34
- interface KafkaJsModule {
35
- Kafka: new (opts: { clientId: string; brokers: string[] }) => KafkaClient;
36
- }
37
-
38
- /**
39
- * Real-Kafka integration test for `KafkaPubSubAdapter` (closes Phase 2.1
40
- * broker-adapter test debt deferred from PR #91).
41
- *
42
- * Exercises both the **fan-out** (no `consumerGroup` — adapter generates
43
- * per-subscriber group ids) and **competing-consumer** (explicit shared
44
- * `consumerGroup`) delivery patterns documented in the adapter.
45
- *
46
- * Gated on `BLOK_INTEGRATION_KAFKA_BROKERS`. Skipped when unset, so the
47
- * regular unit test run on a developer laptop without docker-compose
48
- * doesn't break.
49
- *
50
- * Bring up the test fixtures via:
51
- * docker compose -f infra/testing/docker-compose.yml up -d kafka
52
- *
53
- * Then run:
54
- * BLOK_INTEGRATION_KAFKA_BROKERS=localhost:9094 bun run test
55
- */
56
-
57
- const KAFKA_BROKERS = process.env.BLOK_INTEGRATION_KAFKA_BROKERS;
58
- const d = KAFKA_BROKERS ? describe : describe.skip;
59
-
60
- // CI runners are slower than local — Kafka group coordinator assignment
61
- // alone takes a few seconds for a fresh consumer, and the first message
62
- // after subscribe usually waits one heartbeat. Bump the per-test cap so
63
- // CI doesn't flake on the assignment + first-poll round-trip.
64
- const TEST_TIMEOUT_MS = 45_000;
65
- const SUBSCRIPTION_WARMUP_MS = 2_000;
66
-
67
- d("KafkaPubSubAdapter — real Kafka", () => {
68
- let producer: KafkaPubSubAdapter;
69
- let consumerA: KafkaPubSubAdapter;
70
- let consumerB: KafkaPubSubAdapter;
71
- let admin: KafkaAdminClient | null = null;
72
-
73
- beforeAll(async () => {
74
- const brokers = KAFKA_BROKERS?.split(",").map((s) => s.trim()) ?? [];
75
- producer = new KafkaPubSubAdapter({ brokers, clientId: "blok-test-pubsub-producer" });
76
- await producer.connect();
77
-
78
- consumerA = new KafkaPubSubAdapter({ brokers, clientId: "blok-test-pubsub-consumer-a" });
79
- await consumerA.connect();
80
-
81
- consumerB = new KafkaPubSubAdapter({ brokers, clientId: "blok-test-pubsub-consumer-b" });
82
- await consumerB.connect();
83
-
84
- // Admin client used only by this test to pre-create topics
85
- // before consumers subscribe — eliminates the metadata-propagation
86
- // race where `auto.create.topics.enable` lets a subscribe succeed
87
- // against a topic the producer can't immediately see (kafkajs
88
- // reports "This server does not host this topic-partition" on the
89
- // first publish attempt). The adapter itself relies on auto-create
90
- // in production (Kafka's durable-log model means orphan publishes
91
- // are valid); this guard is purely a test-hygiene measure.
92
- const kafkajs = (await import("kafkajs")) as unknown as KafkaJsModule;
93
- const adminKafka = new kafkajs.Kafka({ clientId: "blok-test-pubsub-admin", brokers });
94
- admin = adminKafka.admin();
95
- await admin.connect();
96
-
97
- // Cold-start warmup. On a freshly booted broker (the CI case),
98
- // Kafka's `__consumer_offsets` topic and the group-coordinator
99
- // election aren't ready immediately after the broker accepts
100
- // admin connections. The first `consumer.run()` that triggers
101
- // `findCoordinator` hits the 5-retry budget before the offsets
102
- // topic is fully initialised and throws
103
- // `KafkaJSNumberOfRetriesExceeded: This is not the correct
104
- // coordinator for this group`.
105
- //
106
- // Drive that initialisation explicitly here: create a throwaway
107
- // topic, run a brief consumer against it (which forces group-
108
- // coordinator election), then disconnect. Subsequent real test
109
- // subscribes find a warm coordinator and skip the race.
110
- const warmupTopic = `blok-test-pubsub-warmup-${Math.random().toString(36).slice(2)}`;
111
- await admin.createTopics({
112
- waitForLeaders: true,
113
- topics: [{ topic: warmupTopic, numPartitions: 1, replicationFactor: 1 }],
114
- });
115
- const warmupConsumer = adminKafka.consumer({ groupId: `blok-test-warmup-${Math.random().toString(36).slice(2)}` });
116
- await warmupConsumer.connect();
117
- await warmupConsumer.subscribe({ topic: warmupTopic, fromBeginning: true });
118
- await warmupConsumer.run({
119
- eachMessage: async () => {
120
- /* never fires — topic stays empty */
121
- },
122
- });
123
- // Give the group coordinator a beat to finalise the join+sync
124
- // for the warmup group. Without this, the next test's subscribe
125
- // can still race on a brand-new group's coordinator lookup.
126
- await new Promise((r) => setTimeout(r, 2_000));
127
- await warmupConsumer.disconnect();
128
- }, TEST_TIMEOUT_MS);
129
-
130
- afterAll(async () => {
131
- // Best-effort cleanup. A failed test can leave a consumer in a
132
- // state where `disconnect()` hangs (kafkajs internal retry loop
133
- // still running). Wrap each call in a 5s timeout so a single
134
- // stuck consumer doesn't trip vitest's default 10s afterAll
135
- // cap and cascade-mask the real failure.
136
- const safeDisconnect = async (label: string, fn: () => Promise<void>): Promise<void> => {
137
- try {
138
- await Promise.race([
139
- fn(),
140
- new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} disconnect timed out`)), 5_000)),
141
- ]);
142
- } catch {
143
- /* ignore — afterAll is best-effort */
144
- }
145
- };
146
- await safeDisconnect("consumerA", () => consumerA.disconnect());
147
- await safeDisconnect("consumerB", () => consumerB.disconnect());
148
- await safeDisconnect("producer", () => producer.disconnect());
149
- if (admin) {
150
- await safeDisconnect("admin", () => admin?.disconnect() ?? Promise.resolve());
151
- }
152
- }, 30_000);
153
-
154
- async function createTopic(topic: string, numPartitions = 1): Promise<void> {
155
- if (!admin) throw new Error("admin client not initialised — beforeAll didn't run");
156
- await admin.createTopics({
157
- waitForLeaders: true,
158
- topics: [{ topic, numPartitions, replicationFactor: 1 }],
159
- });
160
- }
161
-
162
- it(
163
- "fan-out: every subscriber without an explicit consumerGroup receives every message",
164
- async () => {
165
- // Topic name varies per run so the consumer-group offset state
166
- // from a prior run doesn't bleed into this one. Pre-create via
167
- // admin so the producer's metadata is fresh before the first
168
- // publish — bypasses the auto-create-on-publish race that
169
- // shows up as "This server does not host this topic-partition".
170
- const topic = `blok-test-pubsub-fanout-${Math.random().toString(36).slice(2)}`;
171
- await createTopic(topic, 1);
172
- const receivedA: PubSubMessage[] = [];
173
- const receivedB: PubSubMessage[] = [];
174
-
175
- await consumerA.subscribe({ topic, durable: false, startFrom: "earliest" }, async (msg) => {
176
- receivedA.push(msg);
177
- });
178
- await consumerB.subscribe({ topic, durable: false, startFrom: "earliest" }, async (msg) => {
179
- receivedB.push(msg);
180
- });
181
-
182
- // Kafka group-coordinator assignment is asynchronous; the
183
- // `consumer.run` call above returns once the consumer LOOP has
184
- // started, but the first heartbeat-driven assignment isn't done
185
- // yet. Without this warm-up we routinely lose the first publish
186
- // to "no assigned partition" on cold-CI runs.
187
- await new Promise((r) => setTimeout(r, SUBSCRIPTION_WARMUP_MS));
188
-
189
- await producer.publish(topic, { hello: "kafka", n: 1 });
190
-
191
- await waitFor(() => receivedA.length === 1 && receivedB.length === 1, TEST_TIMEOUT_MS - 10_000);
192
-
193
- expect(receivedA[0].body).toEqual({ hello: "kafka", n: 1 });
194
- expect(receivedB[0].body).toEqual({ hello: "kafka", n: 1 });
195
- expect(receivedA[0].topic).toBe(topic);
196
- expect(receivedB[0].topic).toBe(topic);
197
- },
198
- TEST_TIMEOUT_MS,
199
- );
200
-
201
- it(
202
- "competing-consumer: explicit consumerGroup means each message goes to exactly one subscriber",
203
- async () => {
204
- // Three partitions so each consumer in the shared group gets at
205
- // least one assignment — exercises the actual competing-consumer
206
- // load-balance instead of routing all 10 messages to whichever
207
- // consumer happened to win the single-partition leadership.
208
- const topic = `blok-test-pubsub-competing-${Math.random().toString(36).slice(2)}`;
209
- await createTopic(topic, 3);
210
- const group = `blok-test-workers-${Math.random().toString(36).slice(2)}`;
211
- const receivedA: PubSubMessage[] = [];
212
- const receivedB: PubSubMessage[] = [];
213
-
214
- await consumerA.subscribe({ topic, consumerGroup: group, durable: false, startFrom: "earliest" }, async (msg) => {
215
- receivedA.push(msg);
216
- });
217
- await consumerB.subscribe({ topic, consumerGroup: group, durable: false, startFrom: "earliest" }, async (msg) => {
218
- receivedB.push(msg);
219
- });
220
-
221
- await new Promise((r) => setTimeout(r, SUBSCRIPTION_WARMUP_MS));
222
-
223
- // Publish 10 messages with distinct partition keys so the
224
- // broker spreads them across the topic's partitions. The
225
- // auto-created topic defaults to 1 partition, which means BOTH
226
- // consumers join the same group but only ONE gets the partition
227
- // assignment — and therefore all 10 messages. We can't assert a
228
- // 50/50 split, but we CAN assert exactly-once delivery across
229
- // the union.
230
- for (let i = 0; i < 10; i++) {
231
- await producer.publish(topic, { n: i }, { partitionKey: String(i) });
232
- }
233
-
234
- await waitFor(() => receivedA.length + receivedB.length === 10, TEST_TIMEOUT_MS - 10_000);
235
-
236
- expect(receivedA.length + receivedB.length).toBe(10);
237
- const seenN = new Set<number>();
238
- for (const m of [...receivedA, ...receivedB]) {
239
- const n = (m.body as { n: number }).n;
240
- expect(seenN.has(n)).toBe(false);
241
- seenN.add(n);
242
- }
243
- expect(seenN.size).toBe(10);
244
- },
245
- TEST_TIMEOUT_MS,
246
- );
247
-
248
- it(
249
- "publish to a topic with no subscribers does not throw",
250
- async () => {
251
- // Note: kafkajs's auto.create.topics.enable is true on the test
252
- // broker, so this also exercises the on-publish topic-create
253
- // path. The publish should succeed even if no consumer is
254
- // listening — that's the durable-log model Kafka guarantees.
255
- const topic = `blok-test-pubsub-orphan-${Math.random().toString(36).slice(2)}`;
256
- await expect(producer.publish(topic, { dropped: true })).resolves.toBeUndefined();
257
- },
258
- TEST_TIMEOUT_MS,
259
- );
260
- });
261
-
262
- async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
263
- const start = Date.now();
264
- while (Date.now() - start < timeoutMs) {
265
- if (predicate()) return;
266
- await new Promise((r) => setTimeout(r, 50));
267
- }
268
- throw new Error(`waitFor timed out after ${timeoutMs}ms`);
269
- }
@@ -1,138 +0,0 @@
1
- import type { PubSubMessage } from "@blokjs/runner";
2
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
- import { NATSPubSubAdapter } from "../../src/adapters/NATSPubSubAdapter";
4
-
5
- /**
6
- * Real-NATS integration test for `NATSPubSubAdapter` (closes the
7
- * integration test debt from PR #87).
8
- *
9
- * Exercises both the **fan-out** (no `consumerGroup`) and
10
- * **competing-consumer** (with `consumerGroup`) delivery patterns
11
- * documented in the trigger.
12
- *
13
- * Gated on `BLOK_INTEGRATION_NATS_SERVERS`. Skipped when unset.
14
- *
15
- * Bring up the test fixtures via:
16
- * docker compose -f infra/testing/docker-compose.yml up -d nats
17
- */
18
-
19
- const NATS_SERVERS = process.env.BLOK_INTEGRATION_NATS_SERVERS;
20
- const d = NATS_SERVERS ? describe : describe.skip;
21
-
22
- // CI runners are slower than local — JetStream stream + consumer creation
23
- // + cross-instance subscription propagation can take several seconds on
24
- // a cold container. Override the default 5s timeout so CI doesn't flake.
25
- const TEST_TIMEOUT_MS = 30_000;
26
- const SUBSCRIPTION_WARMUP_MS = 500;
27
-
28
- d("NATSPubSubAdapter — real NATS", () => {
29
- let producer: NATSPubSubAdapter;
30
- let consumerA: NATSPubSubAdapter;
31
- let consumerB: NATSPubSubAdapter;
32
-
33
- beforeAll(async () => {
34
- producer = new NATSPubSubAdapter({
35
- servers: NATS_SERVERS?.split(",").map((s) => s.trim()) ?? [],
36
- });
37
- await producer.connect();
38
-
39
- consumerA = new NATSPubSubAdapter({
40
- servers: NATS_SERVERS?.split(",").map((s) => s.trim()) ?? [],
41
- });
42
- await consumerA.connect();
43
-
44
- consumerB = new NATSPubSubAdapter({
45
- servers: NATS_SERVERS?.split(",").map((s) => s.trim()) ?? [],
46
- });
47
- await consumerB.connect();
48
- });
49
-
50
- afterAll(async () => {
51
- await consumerA.disconnect();
52
- await consumerB.disconnect();
53
- await producer.disconnect();
54
- });
55
-
56
- it(
57
- "fan-out: every subscriber without consumerGroup receives every message",
58
- async () => {
59
- const topic = `blok-test-pubsub-fanout-${Math.random().toString(36).slice(2)}`;
60
- const receivedA: PubSubMessage[] = [];
61
- const receivedB: PubSubMessage[] = [];
62
-
63
- await consumerA.subscribe({ topic, durable: false }, async (msg) => {
64
- receivedA.push(msg);
65
- });
66
- await consumerB.subscribe({ topic, durable: false }, async (msg) => {
67
- receivedB.push(msg);
68
- });
69
-
70
- // JetStream subscription registration is asynchronous — give the
71
- // consumer a moment to install before publishing.
72
- await new Promise((r) => setTimeout(r, SUBSCRIPTION_WARMUP_MS));
73
-
74
- await producer.publish(topic, { hello: "world", n: 1 });
75
-
76
- await waitFor(() => receivedA.length === 1 && receivedB.length === 1, TEST_TIMEOUT_MS - 5_000);
77
-
78
- expect(receivedA[0].body).toEqual({ hello: "world", n: 1 });
79
- expect(receivedB[0].body).toEqual({ hello: "world", n: 1 });
80
- },
81
- TEST_TIMEOUT_MS,
82
- );
83
-
84
- it(
85
- "competing-consumer: consumerGroup means each message goes to one subscriber",
86
- async () => {
87
- const topic = `blok-test-pubsub-competing-${Math.random().toString(36).slice(2)}`;
88
- const group = "workers";
89
- const receivedA: PubSubMessage[] = [];
90
- const receivedB: PubSubMessage[] = [];
91
-
92
- await consumerA.subscribe({ topic, consumerGroup: group, durable: false }, async (msg) => {
93
- receivedA.push(msg);
94
- });
95
- await consumerB.subscribe({ topic, consumerGroup: group, durable: false }, async (msg) => {
96
- receivedB.push(msg);
97
- });
98
-
99
- await new Promise((r) => setTimeout(r, SUBSCRIPTION_WARMUP_MS));
100
-
101
- // Publish 10 messages — should split across A + B (not exact 50/50
102
- // but each message reaches exactly one).
103
- for (let i = 0; i < 10; i++) {
104
- await producer.publish(topic, { n: i });
105
- }
106
-
107
- await waitFor(() => receivedA.length + receivedB.length === 10, TEST_TIMEOUT_MS - 5_000);
108
-
109
- expect(receivedA.length + receivedB.length).toBe(10);
110
- // No duplicate keys across both lists.
111
- const seenN = new Set<number>();
112
- for (const m of [...receivedA, ...receivedB]) {
113
- const n = (m.body as { n: number }).n;
114
- expect(seenN.has(n)).toBe(false);
115
- seenN.add(n);
116
- }
117
- },
118
- TEST_TIMEOUT_MS,
119
- );
120
-
121
- it(
122
- "publish to an unsubscribed topic does not throw",
123
- async () => {
124
- const topic = `blok-test-pubsub-orphan-${Math.random().toString(36).slice(2)}`;
125
- await expect(producer.publish(topic, { dropped: true })).resolves.toBeUndefined();
126
- },
127
- TEST_TIMEOUT_MS,
128
- );
129
- });
130
-
131
- async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
132
- const start = Date.now();
133
- while (Date.now() - start < timeoutMs) {
134
- if (predicate()) return;
135
- await new Promise((r) => setTimeout(r, 50));
136
- }
137
- throw new Error(`waitFor timed out after ${timeoutMs}ms`);
138
- }
@@ -1,151 +0,0 @@
1
- /**
2
- * PubSubTrigger Tests
3
- *
4
- * Tests the PubSubTrigger base class and adapter interfaces.
5
- */
6
-
7
- import { describe, expect, it, vi } from "vitest";
8
-
9
- describe("PubSubTrigger", () => {
10
- describe("PubSubMessage Interface", () => {
11
- it("should accept valid pub/sub message structure", () => {
12
- const message = {
13
- id: "msg-123",
14
- body: { event: "order.created", data: { orderId: 456 } },
15
- attributes: { "content-type": "application/json", source: "orders-service" },
16
- raw: {},
17
- topic: "orders",
18
- subscription: "orders-subscription",
19
- publishTime: new Date(),
20
- ack: async () => {},
21
- nack: async () => {},
22
- };
23
-
24
- expect(message.id).toBe("msg-123");
25
- expect(message.body).toEqual({ event: "order.created", data: { orderId: 456 } });
26
- expect(message.topic).toBe("orders");
27
- expect(message.subscription).toBe("orders-subscription");
28
- });
29
-
30
- it("should handle minimal required fields", () => {
31
- const message = {
32
- id: "msg-id",
33
- body: null,
34
- attributes: {},
35
- raw: null,
36
- topic: "test-topic",
37
- ack: async () => {},
38
- nack: async () => {},
39
- };
40
-
41
- expect(message.id).toBeDefined();
42
- expect(message.topic).toBeDefined();
43
- expect(message.ack).toBeDefined();
44
- expect(message.nack).toBeDefined();
45
- });
46
- });
47
-
48
- describe("PubSubAdapter Interface", () => {
49
- it("should validate adapter interface methods", () => {
50
- const mockAdapter = {
51
- provider: "gcp" as const,
52
- connect: vi.fn().mockResolvedValue(undefined),
53
- disconnect: vi.fn().mockResolvedValue(undefined),
54
- subscribe: vi.fn().mockResolvedValue(undefined),
55
- unsubscribe: vi.fn().mockResolvedValue(undefined),
56
- isConnected: vi.fn().mockReturnValue(true),
57
- healthCheck: vi.fn().mockResolvedValue(true),
58
- };
59
-
60
- expect(mockAdapter.provider).toBe("gcp");
61
- expect(typeof mockAdapter.connect).toBe("function");
62
- expect(typeof mockAdapter.disconnect).toBe("function");
63
- expect(typeof mockAdapter.subscribe).toBe("function");
64
- expect(typeof mockAdapter.unsubscribe).toBe("function");
65
- expect(typeof mockAdapter.isConnected).toBe("function");
66
- expect(typeof mockAdapter.healthCheck).toBe("function");
67
- });
68
- });
69
- });
70
-
71
- describe("GCPPubSubAdapter", () => {
72
- it("should create adapter with config from environment", () => {
73
- const originalProject = process.env.GOOGLE_CLOUD_PROJECT;
74
- process.env.GOOGLE_CLOUD_PROJECT = "test-project";
75
-
76
- const config = {
77
- projectId: process.env.GOOGLE_CLOUD_PROJECT,
78
- };
79
-
80
- expect(config.projectId).toBe("test-project");
81
-
82
- process.env.GOOGLE_CLOUD_PROJECT = originalProject;
83
- });
84
- });
85
-
86
- describe("AWSSNSAdapter", () => {
87
- it("should create adapter with config from environment", () => {
88
- const originalRegion = process.env.AWS_REGION;
89
- const originalWaitTime = process.env.SQS_WAIT_TIME_SECONDS;
90
-
91
- process.env.AWS_REGION = "eu-central-1";
92
- process.env.SQS_WAIT_TIME_SECONDS = "15";
93
-
94
- const config = {
95
- region: process.env.AWS_REGION || "us-east-1",
96
- waitTimeSeconds: Number.parseInt(process.env.SQS_WAIT_TIME_SECONDS || "20", 10),
97
- };
98
-
99
- expect(config.region).toBe("eu-central-1");
100
- expect(config.waitTimeSeconds).toBe(15);
101
-
102
- process.env.AWS_REGION = originalRegion;
103
- process.env.SQS_WAIT_TIME_SECONDS = originalWaitTime;
104
- });
105
- });
106
-
107
- describe("AzureServiceBusAdapter", () => {
108
- it("should create adapter with config from environment", () => {
109
- const originalConnStr = process.env.AZURE_SERVICE_BUS_CONNECTION_STRING;
110
- const testConnStr = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=abc";
111
- process.env.AZURE_SERVICE_BUS_CONNECTION_STRING = testConnStr;
112
-
113
- const config = {
114
- connectionString: process.env.AZURE_SERVICE_BUS_CONNECTION_STRING,
115
- };
116
-
117
- expect(config.connectionString).toBe(testConnStr);
118
-
119
- process.env.AZURE_SERVICE_BUS_CONNECTION_STRING = originalConnStr;
120
- });
121
- });
122
-
123
- describe("PubSubTriggerOpts Schema", () => {
124
- it("should validate pub/sub trigger configuration", () => {
125
- const validConfig = {
126
- provider: "gcp" as const,
127
- topic: "my-topic",
128
- subscription: "my-subscription",
129
- ack: true,
130
- maxMessages: 10,
131
- ackDeadline: 30,
132
- };
133
-
134
- expect(validConfig.provider).toBe("gcp");
135
- expect(validConfig.topic).toBe("my-topic");
136
- expect(validConfig.subscription).toBe("my-subscription");
137
- });
138
-
139
- it("should support all provider types", () => {
140
- const providers = ["gcp", "aws", "azure"];
141
-
142
- for (const provider of providers) {
143
- const config = {
144
- provider: provider as any,
145
- topic: "test-topic",
146
- subscription: "test-sub",
147
- };
148
- expect(config.provider).toBe(provider);
149
- }
150
- });
151
- });