@geekmidas/events 0.0.2 → 0.2.0

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.
Files changed (215) hide show
  1. package/dist/{BasicConnection-C4_Y0nIj.d.mts → BasicConnection-BTRsXiuH.d.cts} +3 -2
  2. package/dist/BasicConnection-BTRsXiuH.d.cts.map +1 -0
  3. package/dist/BasicConnection-C-Cg2FJT.cjs.map +1 -1
  4. package/dist/BasicConnection-CpwybcC9.mjs.map +1 -1
  5. package/dist/{BasicConnection-CddnULuT.d.cts → BasicConnection-DIxtA7jK.d.mts} +3 -2
  6. package/dist/BasicConnection-DIxtA7jK.d.mts.map +1 -0
  7. package/dist/BasicPublisher-CXAVKxgq.cjs.map +1 -1
  8. package/dist/{BasicPublisher-DCBxaTwC.d.mts → BasicPublisher-CpJFK2oS.d.mts} +5 -3
  9. package/dist/BasicPublisher-CpJFK2oS.d.mts.map +1 -0
  10. package/dist/{BasicPublisher-V5hELHG9.d.cts → BasicPublisher-D1q0a2xp.d.cts} +5 -3
  11. package/dist/BasicPublisher-D1q0a2xp.d.cts.map +1 -0
  12. package/dist/BasicPublisher-DogbeHAH.mjs.map +1 -1
  13. package/dist/{BasicSubscriber-BB3aKeph.d.mts → BasicSubscriber-6w0Af4DD.d.mts} +5 -3
  14. package/dist/BasicSubscriber-6w0Af4DD.d.mts.map +1 -0
  15. package/dist/{BasicSubscriber-CCKfbRMo.mjs → BasicSubscriber-BJSZN22M.mjs} +2 -4
  16. package/dist/BasicSubscriber-BJSZN22M.mjs.map +1 -0
  17. package/dist/{BasicSubscriber-DOwgFw5j.d.cts → BasicSubscriber-DXxQewjc.d.cts} +5 -3
  18. package/dist/BasicSubscriber-DXxQewjc.d.cts.map +1 -0
  19. package/dist/{BasicSubscriber-BGqv3c8x.cjs → BasicSubscriber-mVJRMXhZ.cjs} +2 -4
  20. package/dist/BasicSubscriber-mVJRMXhZ.cjs.map +1 -0
  21. package/dist/{EventConnection-DZhn1Msl.d.cts → EventConnection-BaJRXdg7.d.cts} +2 -2
  22. package/dist/EventConnection-BaJRXdg7.d.cts.map +1 -0
  23. package/dist/EventConnection-ClLMoIUU.mjs.map +1 -1
  24. package/dist/EventConnection-F_N81SXu.cjs.map +1 -1
  25. package/dist/{EventConnection-DeXfLGom.d.mts → EventConnection-SOhtmxib.d.mts} +2 -2
  26. package/dist/EventConnection-SOhtmxib.d.mts.map +1 -0
  27. package/dist/EventConnection.d.cts +2 -2
  28. package/dist/EventConnection.d.mts +2 -2
  29. package/dist/{Publisher-C3m8JFXm.d.mts → Publisher-1CfcYRnm.d.mts} +4 -2
  30. package/dist/Publisher-1CfcYRnm.d.mts.map +1 -0
  31. package/dist/{Publisher-nXNVjL0e.d.cts → Publisher-BfNgvHwQ.d.cts} +4 -2
  32. package/dist/Publisher-BfNgvHwQ.d.cts.map +1 -0
  33. package/dist/Publisher-DS0aOh8r.mjs.map +1 -1
  34. package/dist/Publisher-ceu6OxCj.cjs.map +1 -1
  35. package/dist/Publisher.d.cts +2 -2
  36. package/dist/Publisher.d.mts +2 -2
  37. package/dist/{RabbitMQConnection-ungB0Zl6.cjs → RabbitMQConnection-Blj9Lo05.cjs} +2 -2
  38. package/dist/RabbitMQConnection-Blj9Lo05.cjs.map +1 -0
  39. package/dist/{RabbitMQConnection-7cWp_-_Q.d.cts → RabbitMQConnection-Bm8VCI2R.d.cts} +3 -2
  40. package/dist/RabbitMQConnection-Bm8VCI2R.d.cts.map +1 -0
  41. package/dist/{RabbitMQConnection-C2HC7J7K.d.mts → RabbitMQConnection-Ck-ktCp_.d.mts} +3 -2
  42. package/dist/RabbitMQConnection-Ck-ktCp_.d.mts.map +1 -0
  43. package/dist/{RabbitMQConnection-kU3MvN5P.mjs → RabbitMQConnection-Co7f5Xsx.mjs} +2 -2
  44. package/dist/RabbitMQConnection-Co7f5Xsx.mjs.map +1 -0
  45. package/dist/RabbitMQPublisher--DL6GM6g.cjs.map +1 -1
  46. package/dist/{RabbitMQPublisher-CpcbgrXj.d.cts → RabbitMQPublisher-COnxcWEZ.d.mts} +5 -3
  47. package/dist/RabbitMQPublisher-COnxcWEZ.d.mts.map +1 -0
  48. package/dist/RabbitMQPublisher-o_oVI3wr.mjs.map +1 -1
  49. package/dist/{RabbitMQPublisher-DDZi15Ro.d.mts → RabbitMQPublisher-qQT7-1-p.d.cts} +5 -3
  50. package/dist/RabbitMQPublisher-qQT7-1-p.d.cts.map +1 -0
  51. package/dist/{RabbitMQSubscriber-BplEJEk8.cjs → RabbitMQSubscriber-C8MYAihn.cjs} +2 -3
  52. package/dist/RabbitMQSubscriber-C8MYAihn.cjs.map +1 -0
  53. package/dist/{RabbitMQSubscriber-BJ9PTGny.d.cts → RabbitMQSubscriber-C_sZjDPU.d.mts} +5 -3
  54. package/dist/RabbitMQSubscriber-C_sZjDPU.d.mts.map +1 -0
  55. package/dist/{RabbitMQSubscriber-CIvIw8MG.d.mts → RabbitMQSubscriber-D12sssMb.d.cts} +5 -3
  56. package/dist/RabbitMQSubscriber-D12sssMb.d.cts.map +1 -0
  57. package/dist/{RabbitMQSubscriber-BaXqs5nO.mjs → RabbitMQSubscriber-DlGL0pC2.mjs} +2 -3
  58. package/dist/RabbitMQSubscriber-DlGL0pC2.mjs.map +1 -0
  59. package/dist/{SNSConnection-CAImaMM2.d.mts → SNSConnection-BDfQrVBO.d.cts} +3 -2
  60. package/dist/SNSConnection-BDfQrVBO.d.cts.map +1 -0
  61. package/dist/SNSConnection-D7wCf9Si.mjs.map +1 -1
  62. package/dist/SNSConnection-DVsC_pbB.cjs.map +1 -1
  63. package/dist/{SNSConnection-Cqbr7-Ho.d.cts → SNSConnection-mHb10nQ6.d.mts} +3 -2
  64. package/dist/SNSConnection-mHb10nQ6.d.mts.map +1 -0
  65. package/dist/{SNSPublisher-BZ6N3JI9.cjs → SNSPublisher-BoA8uLnw.cjs} +2 -3
  66. package/dist/SNSPublisher-BoA8uLnw.cjs.map +1 -0
  67. package/dist/{SNSPublisher-Dkhb4LU-.d.mts → SNSPublisher-CM7UAWtf.d.mts} +7 -6
  68. package/dist/SNSPublisher-CM7UAWtf.d.mts.map +1 -0
  69. package/dist/{SNSPublisher-BfWjzNiB.mjs → SNSPublisher-Drr4kHn6.mjs} +2 -3
  70. package/dist/SNSPublisher-Drr4kHn6.mjs.map +1 -0
  71. package/dist/{SNSPublisher-RVZFa7kL.d.cts → SNSPublisher-kpsSlGDn.d.cts} +7 -6
  72. package/dist/SNSPublisher-kpsSlGDn.d.cts.map +1 -0
  73. package/dist/{SNSSubscriber-C9YJWa7m.mjs → SNSSubscriber-CBIvQpqK.mjs} +4 -8
  74. package/dist/SNSSubscriber-CBIvQpqK.mjs.map +1 -0
  75. package/dist/{SNSSubscriber-UE0ia4XM.cjs → SNSSubscriber-CWiAJjpU.cjs} +4 -8
  76. package/dist/SNSSubscriber-CWiAJjpU.cjs.map +1 -0
  77. package/dist/{SNSSubscriber-B31JmkAE.d.mts → SNSSubscriber-DDB3--pm.d.cts} +4 -3
  78. package/dist/SNSSubscriber-DDB3--pm.d.cts.map +1 -0
  79. package/dist/{SNSSubscriber-k2UwKUxu.d.cts → SNSSubscriber-Dwo8391f.d.mts} +4 -3
  80. package/dist/SNSSubscriber-Dwo8391f.d.mts.map +1 -0
  81. package/dist/{SQSConnection-C5WFqZW7.d.mts → SQSConnection-B5Ep9Tma.d.cts} +3 -2
  82. package/dist/SQSConnection-B5Ep9Tma.d.cts.map +1 -0
  83. package/dist/{SQSConnection-geav2Rsn.d.cts → SQSConnection-BI_xOzyJ.d.mts} +3 -2
  84. package/dist/SQSConnection-BI_xOzyJ.d.mts.map +1 -0
  85. package/dist/SQSConnection-BwNjUPr4.cjs.map +1 -1
  86. package/dist/SQSConnection-Eleqqre3.mjs.map +1 -1
  87. package/dist/{SQSPublisher-CdsayOLV.d.mts → SQSPublisher-BN1ds6SN.d.mts} +5 -3
  88. package/dist/SQSPublisher-BN1ds6SN.d.mts.map +1 -0
  89. package/dist/{SQSPublisher-Utb_FTe5.d.cts → SQSPublisher-DFTqwzRW.d.cts} +5 -3
  90. package/dist/SQSPublisher-DFTqwzRW.d.cts.map +1 -0
  91. package/dist/SQSPublisher-DG9o4ME3.mjs.map +1 -1
  92. package/dist/SQSPublisher-Dq5353oZ.cjs.map +1 -1
  93. package/dist/{SQSSubscriber-DZDNgLE5.cjs → SQSSubscriber-B4sRXVl6.cjs} +5 -12
  94. package/dist/SQSSubscriber-B4sRXVl6.cjs.map +1 -0
  95. package/dist/{SQSSubscriber-B6iL1EnX.d.cts → SQSSubscriber-BO2me_gs.d.cts} +4 -3
  96. package/dist/SQSSubscriber-BO2me_gs.d.cts.map +1 -0
  97. package/dist/{SQSSubscriber-CD66P7hH.mjs → SQSSubscriber-CVGhZ8Nr.mjs} +5 -12
  98. package/dist/SQSSubscriber-CVGhZ8Nr.mjs.map +1 -0
  99. package/dist/{SQSSubscriber-DBeE4XAR.d.mts → SQSSubscriber-FyZFfr2-.d.mts} +4 -3
  100. package/dist/SQSSubscriber-FyZFfr2-.d.mts.map +1 -0
  101. package/dist/Subscriber-Bd9hb_jp.mjs.map +1 -1
  102. package/dist/{Subscriber-Cff9EIpv.d.mts → Subscriber-BxT-KzFJ.d.mts} +4 -2
  103. package/dist/Subscriber-BxT-KzFJ.d.mts.map +1 -0
  104. package/dist/{Subscriber-CpAwcUzb.d.cts → Subscriber-DDD_4Nlx.d.cts} +4 -2
  105. package/dist/Subscriber-DDD_4Nlx.d.cts.map +1 -0
  106. package/dist/Subscriber-saVOWuMq.cjs.map +1 -1
  107. package/dist/Subscriber.d.cts +2 -2
  108. package/dist/Subscriber.d.mts +2 -2
  109. package/dist/basic/BasicConnection.d.cts +2 -2
  110. package/dist/basic/BasicConnection.d.mts +2 -2
  111. package/dist/basic/BasicPublisher.d.cts +3 -3
  112. package/dist/basic/BasicPublisher.d.mts +3 -3
  113. package/dist/basic/BasicSubscriber.cjs +1 -1
  114. package/dist/basic/BasicSubscriber.d.cts +3 -3
  115. package/dist/basic/BasicSubscriber.d.mts +3 -3
  116. package/dist/basic/BasicSubscriber.mjs +1 -1
  117. package/dist/basic/index.cjs +1 -1
  118. package/dist/basic/index.d.cts +4 -4
  119. package/dist/basic/index.d.mts +4 -4
  120. package/dist/basic/index.mjs +1 -1
  121. package/dist/index.d.cts +4 -4
  122. package/dist/index.d.mts +4 -4
  123. package/dist/rabbitmq/RabbitMQConnection.cjs +1 -1
  124. package/dist/rabbitmq/RabbitMQConnection.d.cts +2 -2
  125. package/dist/rabbitmq/RabbitMQConnection.d.mts +2 -2
  126. package/dist/rabbitmq/RabbitMQConnection.mjs +1 -1
  127. package/dist/rabbitmq/RabbitMQPublisher.d.cts +3 -3
  128. package/dist/rabbitmq/RabbitMQPublisher.d.mts +3 -3
  129. package/dist/rabbitmq/RabbitMQSubscriber.cjs +1 -1
  130. package/dist/rabbitmq/RabbitMQSubscriber.d.cts +3 -3
  131. package/dist/rabbitmq/RabbitMQSubscriber.d.mts +3 -3
  132. package/dist/rabbitmq/RabbitMQSubscriber.mjs +1 -1
  133. package/dist/rabbitmq/index.cjs +2 -2
  134. package/dist/rabbitmq/index.d.cts +4 -4
  135. package/dist/rabbitmq/index.d.mts +4 -4
  136. package/dist/rabbitmq/index.mjs +2 -2
  137. package/dist/sns/SNSConnection.d.cts +2 -2
  138. package/dist/sns/SNSConnection.d.mts +2 -2
  139. package/dist/sns/SNSPublisher.cjs +1 -1
  140. package/dist/sns/SNSPublisher.d.cts +3 -3
  141. package/dist/sns/SNSPublisher.d.mts +3 -3
  142. package/dist/sns/SNSPublisher.mjs +1 -1
  143. package/dist/sns/SNSSubscriber.cjs +2 -2
  144. package/dist/sns/SNSSubscriber.d.cts +3 -3
  145. package/dist/sns/SNSSubscriber.d.mts +3 -3
  146. package/dist/sns/SNSSubscriber.mjs +2 -2
  147. package/dist/sns/index.cjs +3 -3
  148. package/dist/sns/index.d.cts +4 -4
  149. package/dist/sns/index.d.mts +4 -4
  150. package/dist/sns/index.mjs +3 -3
  151. package/dist/sqs/SQSConnection.d.cts +2 -2
  152. package/dist/sqs/SQSConnection.d.mts +2 -2
  153. package/dist/sqs/SQSPublisher.d.cts +3 -3
  154. package/dist/sqs/SQSPublisher.d.mts +3 -3
  155. package/dist/sqs/SQSSubscriber.cjs +1 -1
  156. package/dist/sqs/SQSSubscriber.d.cts +3 -3
  157. package/dist/sqs/SQSSubscriber.d.mts +3 -3
  158. package/dist/sqs/SQSSubscriber.mjs +1 -1
  159. package/dist/sqs/index.cjs +1 -1
  160. package/dist/sqs/index.d.cts +4 -4
  161. package/dist/sqs/index.d.mts +4 -4
  162. package/dist/sqs/index.mjs +1 -1
  163. package/dist/types-B63qHVOG.mjs.map +1 -1
  164. package/dist/types-CvSMEO5z.cjs.map +1 -1
  165. package/dist/{types-D0g-o6wX.d.mts → types-DSVsPgKd.d.mts} +2 -1
  166. package/dist/types-DSVsPgKd.d.mts.map +1 -0
  167. package/dist/{types-DEGUg5BA.d.cts → types-Dmbp1U-6.d.cts} +2 -1
  168. package/dist/types-Dmbp1U-6.d.cts.map +1 -0
  169. package/dist/types.d.cts +1 -1
  170. package/dist/types.d.mts +1 -1
  171. package/package.json +24 -5
  172. package/src/EventConnection.ts +31 -31
  173. package/src/Publisher.ts +78 -78
  174. package/src/Subscriber.ts +91 -91
  175. package/src/__tests__/EventConnection.spec.ts +42 -0
  176. package/src/__tests__/Publisher.spec.ts +96 -0
  177. package/src/__tests__/Subscriber.spec.ts +91 -23
  178. package/src/basic/BasicConnection.ts +35 -35
  179. package/src/basic/BasicPublisher.ts +8 -8
  180. package/src/basic/BasicSubscriber.ts +13 -16
  181. package/src/basic/__tests__/BasicSubscriber.spec.ts +116 -116
  182. package/src/index.ts +13 -13
  183. package/src/rabbitmq/RabbitMQConnection.ts +139 -139
  184. package/src/rabbitmq/RabbitMQPublisher.ts +48 -48
  185. package/src/rabbitmq/RabbitMQSubscriber.ts +100 -102
  186. package/src/rabbitmq/__tests__/RabbitMQPublisher.spec.ts +364 -364
  187. package/src/rabbitmq/__tests__/RabbitMQSubscriber.spec.ts +204 -204
  188. package/src/rabbitmq/index.ts +3 -3
  189. package/src/sns/SNSConnection.ts +76 -76
  190. package/src/sns/SNSPublisher.ts +54 -56
  191. package/src/sns/SNSSubscriber.ts +187 -191
  192. package/src/sns/__tests__/SNS-SQS-Integration.spec.ts +321 -321
  193. package/src/sns/__tests__/SNSPublisher.spec.ts +449 -0
  194. package/src/sns/__tests__/SNSSubscriber.spec.ts +527 -0
  195. package/src/sns/index.ts +3 -3
  196. package/src/sqs/SQSConnection.ts +75 -75
  197. package/src/sqs/SQSPublisher.ts +93 -93
  198. package/src/sqs/SQSSubscriber.ts +157 -165
  199. package/src/sqs/__tests__/SQSPublisher.spec.ts +178 -178
  200. package/src/sqs/index.ts +3 -3
  201. package/src/types.ts +28 -29
  202. package/tsconfig.json +9 -0
  203. package/tsdown.config.ts +1 -1
  204. package/dist/BasicSubscriber-BGqv3c8x.cjs.map +0 -1
  205. package/dist/BasicSubscriber-CCKfbRMo.mjs.map +0 -1
  206. package/dist/RabbitMQConnection-kU3MvN5P.mjs.map +0 -1
  207. package/dist/RabbitMQConnection-ungB0Zl6.cjs.map +0 -1
  208. package/dist/RabbitMQSubscriber-BaXqs5nO.mjs.map +0 -1
  209. package/dist/RabbitMQSubscriber-BplEJEk8.cjs.map +0 -1
  210. package/dist/SNSPublisher-BZ6N3JI9.cjs.map +0 -1
  211. package/dist/SNSPublisher-BfWjzNiB.mjs.map +0 -1
  212. package/dist/SNSSubscriber-C9YJWa7m.mjs.map +0 -1
  213. package/dist/SNSSubscriber-UE0ia4XM.cjs.map +0 -1
  214. package/dist/SQSSubscriber-CD66P7hH.mjs.map +0 -1
  215. package/dist/SQSSubscriber-DZDNgLE5.cjs.map +0 -1
@@ -0,0 +1,449 @@
1
+ import {
2
+ CreateTopicCommand,
3
+ DeleteTopicCommand,
4
+ SNSClient,
5
+ SubscribeCommand,
6
+ UnsubscribeCommand,
7
+ } from '@aws-sdk/client-sns';
8
+ import {
9
+ CreateQueueCommand,
10
+ DeleteQueueCommand,
11
+ GetQueueAttributesCommand,
12
+ ReceiveMessageCommand,
13
+ SetQueueAttributesCommand,
14
+ SQSClient,
15
+ } from '@aws-sdk/client-sqs';
16
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
17
+ import type { PublishableMessage } from '../../types';
18
+ import { SNSConnection } from '../SNSConnection';
19
+ import { SNSPublisher } from '../SNSPublisher';
20
+
21
+ type TestMessage = PublishableMessage<
22
+ 'user.created' | 'user.updated' | 'order.placed',
23
+ any
24
+ >;
25
+
26
+ const LOCALSTACK_ENDPOINT = 'http://localhost:4566';
27
+ const AWS_REGION = 'us-east-1';
28
+ const AWS_CREDENTIALS = {
29
+ accessKeyId: 'test',
30
+ secretAccessKey: 'test',
31
+ };
32
+
33
+ // Test infrastructure helpers
34
+ async function createTopic(topicName: string): Promise<string> {
35
+ const client = new SNSClient({
36
+ region: AWS_REGION,
37
+ endpoint: LOCALSTACK_ENDPOINT,
38
+ credentials: AWS_CREDENTIALS,
39
+ });
40
+
41
+ const command = new CreateTopicCommand({ Name: topicName });
42
+ const response = await client.send(command);
43
+ client.destroy();
44
+
45
+ if (!response.TopicArn) {
46
+ throw new Error('Failed to create topic');
47
+ }
48
+
49
+ return response.TopicArn;
50
+ }
51
+
52
+ async function deleteTopic(topicArn: string): Promise<void> {
53
+ const client = new SNSClient({
54
+ region: AWS_REGION,
55
+ endpoint: LOCALSTACK_ENDPOINT,
56
+ credentials: AWS_CREDENTIALS,
57
+ });
58
+
59
+ await client.send(new DeleteTopicCommand({ TopicArn: topicArn }));
60
+ client.destroy();
61
+ }
62
+
63
+ async function createQueue(queueName: string): Promise<string> {
64
+ const client = new SQSClient({
65
+ region: AWS_REGION,
66
+ endpoint: LOCALSTACK_ENDPOINT,
67
+ credentials: AWS_CREDENTIALS,
68
+ });
69
+
70
+ const response = await client.send(
71
+ new CreateQueueCommand({ QueueName: queueName }),
72
+ );
73
+ client.destroy();
74
+
75
+ if (!response.QueueUrl) {
76
+ throw new Error('Failed to create queue');
77
+ }
78
+
79
+ return response.QueueUrl;
80
+ }
81
+
82
+ async function deleteQueue(queueUrl: string): Promise<void> {
83
+ const client = new SQSClient({
84
+ region: AWS_REGION,
85
+ endpoint: LOCALSTACK_ENDPOINT,
86
+ credentials: AWS_CREDENTIALS,
87
+ });
88
+
89
+ await client.send(new DeleteQueueCommand({ QueueUrl: queueUrl }));
90
+ client.destroy();
91
+ }
92
+
93
+ async function getQueueArn(queueUrl: string): Promise<string> {
94
+ const client = new SQSClient({
95
+ region: AWS_REGION,
96
+ endpoint: LOCALSTACK_ENDPOINT,
97
+ credentials: AWS_CREDENTIALS,
98
+ });
99
+
100
+ const response = await client.send(
101
+ new GetQueueAttributesCommand({
102
+ QueueUrl: queueUrl,
103
+ AttributeNames: ['QueueArn'],
104
+ }),
105
+ );
106
+ client.destroy();
107
+
108
+ const queueArn = response.Attributes?.QueueArn;
109
+ if (!queueArn) {
110
+ throw new Error('Failed to get queue ARN');
111
+ }
112
+
113
+ return queueArn;
114
+ }
115
+
116
+ async function setQueuePolicy(
117
+ queueUrl: string,
118
+ queueArn: string,
119
+ topicArn: string,
120
+ ): Promise<void> {
121
+ const client = new SQSClient({
122
+ region: AWS_REGION,
123
+ endpoint: LOCALSTACK_ENDPOINT,
124
+ credentials: AWS_CREDENTIALS,
125
+ });
126
+
127
+ const policy = {
128
+ Version: '2012-10-17',
129
+ Statement: [
130
+ {
131
+ Effect: 'Allow',
132
+ Principal: '*',
133
+ Action: 'SQS:SendMessage',
134
+ Resource: queueArn,
135
+ Condition: {
136
+ ArnEquals: { 'aws:SourceArn': topicArn },
137
+ },
138
+ },
139
+ ],
140
+ };
141
+
142
+ await client.send(
143
+ new SetQueueAttributesCommand({
144
+ QueueUrl: queueUrl,
145
+ Attributes: { Policy: JSON.stringify(policy) },
146
+ }),
147
+ );
148
+ client.destroy();
149
+ }
150
+
151
+ async function subscribeQueueToTopic(
152
+ topicArn: string,
153
+ queueArn: string,
154
+ ): Promise<string> {
155
+ const client = new SNSClient({
156
+ region: AWS_REGION,
157
+ endpoint: LOCALSTACK_ENDPOINT,
158
+ credentials: AWS_CREDENTIALS,
159
+ });
160
+
161
+ const response = await client.send(
162
+ new SubscribeCommand({
163
+ TopicArn: topicArn,
164
+ Protocol: 'sqs',
165
+ Endpoint: queueArn,
166
+ }),
167
+ );
168
+ client.destroy();
169
+
170
+ if (!response.SubscriptionArn) {
171
+ throw new Error('Failed to subscribe queue to topic');
172
+ }
173
+
174
+ return response.SubscriptionArn;
175
+ }
176
+
177
+ async function unsubscribe(subscriptionArn: string): Promise<void> {
178
+ const client = new SNSClient({
179
+ region: AWS_REGION,
180
+ endpoint: LOCALSTACK_ENDPOINT,
181
+ credentials: AWS_CREDENTIALS,
182
+ });
183
+
184
+ await client.send(
185
+ new UnsubscribeCommand({ SubscriptionArn: subscriptionArn }),
186
+ );
187
+ client.destroy();
188
+ }
189
+
190
+ async function receiveMessages(queueUrl: string): Promise<any[]> {
191
+ const client = new SQSClient({
192
+ region: AWS_REGION,
193
+ endpoint: LOCALSTACK_ENDPOINT,
194
+ credentials: AWS_CREDENTIALS,
195
+ });
196
+
197
+ const response = await client.send(
198
+ new ReceiveMessageCommand({
199
+ QueueUrl: queueUrl,
200
+ WaitTimeSeconds: 5,
201
+ MaxNumberOfMessages: 10,
202
+ MessageAttributeNames: ['All'],
203
+ }),
204
+ );
205
+ client.destroy();
206
+
207
+ return (response.Messages || []).map((msg) => {
208
+ // SNS wraps the message in its own envelope
209
+ const snsEnvelope = JSON.parse(msg.Body || '{}');
210
+ return JSON.parse(snsEnvelope.Message || '{}');
211
+ });
212
+ }
213
+
214
+ describe('SNSPublisher', () => {
215
+ const uniqueId = () =>
216
+ `${Date.now()}-${Math.random().toString(36).substring(7)}`;
217
+
218
+ describe('constructor', () => {
219
+ it('should create publisher with connection', async () => {
220
+ const topicArn = await createTopic(`test-constructor-${uniqueId()}`);
221
+
222
+ try {
223
+ const connection = new SNSConnection({
224
+ topicArn,
225
+ endpoint: LOCALSTACK_ENDPOINT,
226
+ region: AWS_REGION,
227
+ credentials: AWS_CREDENTIALS,
228
+ });
229
+ await connection.connect();
230
+
231
+ const publisher = new SNSPublisher<TestMessage>(connection);
232
+ expect(publisher).toBeDefined();
233
+
234
+ await connection.close();
235
+ } finally {
236
+ await deleteTopic(topicArn);
237
+ }
238
+ });
239
+ });
240
+
241
+ describe('publish', () => {
242
+ let topicArn: string;
243
+ let queueUrl: string;
244
+ let queueArn: string;
245
+ let subscriptionArn: string;
246
+ let connection: SNSConnection;
247
+
248
+ beforeAll(async () => {
249
+ topicArn = await createTopic(`test-publish-${uniqueId()}`);
250
+ queueUrl = await createQueue(`test-publish-queue-${uniqueId()}`);
251
+ queueArn = await getQueueArn(queueUrl);
252
+ await setQueuePolicy(queueUrl, queueArn, topicArn);
253
+ subscriptionArn = await subscribeQueueToTopic(topicArn, queueArn);
254
+
255
+ // Wait for subscription to be ready
256
+ await new Promise((resolve) => setTimeout(resolve, 500));
257
+
258
+ connection = new SNSConnection({
259
+ topicArn,
260
+ endpoint: LOCALSTACK_ENDPOINT,
261
+ region: AWS_REGION,
262
+ credentials: AWS_CREDENTIALS,
263
+ });
264
+ await connection.connect();
265
+ });
266
+
267
+ afterAll(async () => {
268
+ await connection.close();
269
+ await unsubscribe(subscriptionArn);
270
+ await deleteQueue(queueUrl);
271
+ await deleteTopic(topicArn);
272
+ });
273
+
274
+ it('should return early if messages array is empty', async () => {
275
+ const publisher = new SNSPublisher<TestMessage>(connection);
276
+
277
+ // Should complete immediately without error or sending anything
278
+ const startTime = Date.now();
279
+ await publisher.publish([]);
280
+ const elapsed = Date.now() - startTime;
281
+
282
+ // Should complete almost instantly (< 100ms) since no messages are sent
283
+ expect(elapsed).toBeLessThan(100);
284
+ });
285
+
286
+ it('should publish a single message with correct format', async () => {
287
+ const publisher = new SNSPublisher<TestMessage>(connection);
288
+
289
+ await publisher.publish([
290
+ {
291
+ type: 'user.created',
292
+ payload: { userId: '123', email: 'test@example.com' },
293
+ },
294
+ ]);
295
+
296
+ // Wait for message to arrive
297
+ await new Promise((resolve) => setTimeout(resolve, 1000));
298
+
299
+ const messages = await receiveMessages(queueUrl);
300
+ expect(messages).toHaveLength(1);
301
+ expect(messages[0].type).toBe('user.created');
302
+ expect(messages[0].payload.userId).toBe('123');
303
+ expect(messages[0].payload.email).toBe('test@example.com');
304
+ });
305
+
306
+ it('should publish multiple messages', async () => {
307
+ // Create fresh queue to avoid picking up old messages
308
+ const freshQueueUrl = await createQueue(`test-multi-${uniqueId()}`);
309
+ const freshQueueArn = await getQueueArn(freshQueueUrl);
310
+ await setQueuePolicy(freshQueueUrl, freshQueueArn, topicArn);
311
+ const freshSubscriptionArn = await subscribeQueueToTopic(
312
+ topicArn,
313
+ freshQueueArn,
314
+ );
315
+
316
+ await new Promise((resolve) => setTimeout(resolve, 500));
317
+
318
+ try {
319
+ const publisher = new SNSPublisher<TestMessage>(connection);
320
+
321
+ await publisher.publish([
322
+ { type: 'user.created', payload: { userId: '1' } },
323
+ { type: 'user.updated', payload: { userId: '2' } },
324
+ { type: 'order.placed', payload: { orderId: '3' } },
325
+ ]);
326
+
327
+ await new Promise((resolve) => setTimeout(resolve, 1500));
328
+
329
+ const messages = await receiveMessages(freshQueueUrl);
330
+ expect(messages).toHaveLength(3);
331
+
332
+ const types = messages.map((m) => m.type).sort();
333
+ expect(types).toEqual(['order.placed', 'user.created', 'user.updated']);
334
+ } finally {
335
+ await unsubscribe(freshSubscriptionArn);
336
+ await deleteQueue(freshQueueUrl);
337
+ }
338
+ });
339
+
340
+ it('should handle complex payloads', async () => {
341
+ const freshQueueUrl = await createQueue(`test-complex-${uniqueId()}`);
342
+ const freshQueueArn = await getQueueArn(freshQueueUrl);
343
+ await setQueuePolicy(freshQueueUrl, freshQueueArn, topicArn);
344
+ const freshSubscriptionArn = await subscribeQueueToTopic(
345
+ topicArn,
346
+ freshQueueArn,
347
+ );
348
+
349
+ await new Promise((resolve) => setTimeout(resolve, 500));
350
+
351
+ try {
352
+ const publisher = new SNSPublisher<TestMessage>(connection);
353
+ const complexPayload = {
354
+ userId: '123',
355
+ nested: {
356
+ data: [1, 2, 3],
357
+ metadata: { key: 'value' },
358
+ },
359
+ timestamp: '2024-01-01T00:00:00Z',
360
+ };
361
+
362
+ await publisher.publish([
363
+ { type: 'user.created', payload: complexPayload },
364
+ ]);
365
+
366
+ await new Promise((resolve) => setTimeout(resolve, 1000));
367
+
368
+ const messages = await receiveMessages(freshQueueUrl);
369
+ expect(messages).toHaveLength(1);
370
+ expect(messages[0].payload).toEqual(complexPayload);
371
+ } finally {
372
+ await unsubscribe(freshSubscriptionArn);
373
+ await deleteQueue(freshQueueUrl);
374
+ }
375
+ });
376
+
377
+ it('should auto-connect if not connected', async () => {
378
+ const freshConnection = new SNSConnection({
379
+ topicArn,
380
+ endpoint: LOCALSTACK_ENDPOINT,
381
+ region: AWS_REGION,
382
+ credentials: AWS_CREDENTIALS,
383
+ });
384
+
385
+ // Don't call connect() explicitly
386
+ expect(freshConnection.isConnected()).toBe(false);
387
+
388
+ const publisher = new SNSPublisher<TestMessage>(freshConnection);
389
+
390
+ // Should auto-connect and publish
391
+ await publisher.publish([
392
+ { type: 'user.created', payload: { test: 'auto-connect' } },
393
+ ]);
394
+
395
+ expect(freshConnection.isConnected()).toBe(true);
396
+
397
+ await freshConnection.close();
398
+ });
399
+ });
400
+
401
+ describe('fromConnectionString', () => {
402
+ it('should create publisher from connection string', async () => {
403
+ const topicArn = await createTopic(`test-fromcs-${uniqueId()}`);
404
+
405
+ try {
406
+ const connectionString = `sns://?topicArn=${encodeURIComponent(topicArn)}&region=${AWS_REGION}&endpoint=${encodeURIComponent(LOCALSTACK_ENDPOINT)}&accessKeyId=test&secretAccessKey=test`;
407
+
408
+ const publisher =
409
+ await SNSPublisher.fromConnectionString<TestMessage>(
410
+ connectionString,
411
+ );
412
+
413
+ expect(publisher).toBeInstanceOf(SNSPublisher);
414
+
415
+ await publisher.close();
416
+ } finally {
417
+ await deleteTopic(topicArn);
418
+ }
419
+ });
420
+ });
421
+
422
+ describe('close', () => {
423
+ it('should complete without error (publisher does not own connection)', async () => {
424
+ const topicArn = await createTopic(`test-close-${uniqueId()}`);
425
+
426
+ try {
427
+ const connection = new SNSConnection({
428
+ topicArn,
429
+ endpoint: LOCALSTACK_ENDPOINT,
430
+ region: AWS_REGION,
431
+ credentials: AWS_CREDENTIALS,
432
+ });
433
+ await connection.connect();
434
+
435
+ const publisher = new SNSPublisher<TestMessage>(connection);
436
+
437
+ // close() should not throw and should not close the connection
438
+ await publisher.close();
439
+
440
+ // Connection should still be open
441
+ expect(connection.isConnected()).toBe(true);
442
+
443
+ await connection.close();
444
+ } finally {
445
+ await deleteTopic(topicArn);
446
+ }
447
+ });
448
+ });
449
+ });