-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathMain.hs
424 lines (370 loc) · 13.3 KB
/
Main.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE CPP #-}
-- |
-- Module: Main
-- Copyright: Copyright © 2014 AlephCloud Systems, Inc.
-- License: MIT
-- Maintainer: Lars Kuhtz <lars@alephcloud.com>
-- Stability: experimental
--
-- Tests for Haskell SNS bindings
--
module Main
( main
) where
import Aws
import Aws.Core
import Aws.General
import Aws.Sns
import qualified Aws.Sqs as SQS
import Control.Arrow (second)
import Control.Concurrent (threadDelay)
import Control.Error
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Data.Aeson (encode, object, (.=), eitherDecode)
import qualified Data.ByteString.Lazy as LB
import qualified Data.List as L
import Data.Monoid
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Test.Tasty
import System.Environment
import System.Exit
import System.IO
import Utils
-- -------------------------------------------------------------------------- --
-- Main
main :: IO ()
main = do
args <- getArgs
runMain args $ map (second tail . span (/= '=')) args
where
runMain :: [String] -> [(String,String)] -> IO ()
runMain args argsMap
| any (`elem` helpArgs) args = defaultMain (tests "")
| "--run-with-aws-credentials" `elem` args =
case lookup "--test-email" argsMap of
Nothing -> do
hPutStrLn stderr "Command line option --test-email=<email> is missing"
hPutStrLn stderr help
exitFailure
Just email ->
withArgs (tastyArgs args) . defaultMain $ tests (T.pack email)
| otherwise = putStrLn help >> exitFailure
helpArgs = ["--help", "-h"]
mainArgs =
[ "--run-with-aws-credentials"
, "--test-email"
]
tastyArgs args = flip filter args $ \x -> not
$ any (`L.isPrefixOf` x) mainArgs
help :: String
help = L.intercalate "\n"
[ ""
, "NOTE"
, ""
, "This test suite accesses the AWS account that is associated with"
, "the default credentials from the credential file ~/.aws-keys."
, ""
, "By running the tests in this test-suite costs for usage of AWS"
, "services may incur."
, ""
, "In order to actually excute the tests in this test-suite you must"
, "provide the command line options:"
, ""
, " --test-email=<email-address>"
, " --run-with-aws-credentials"
, ""
, "When running this test-suite through cabal you may use the following"
, "command:"
, ""
, " cabal test SNS-tests --test-option=--run-with-aws-credentials \\"
, " --test-option=--test-email=<email-address>"
, ""
]
tests
:: T.Text -- email address
-> TestTree
tests email = testGroup "SNS Tests"
[ test_createTopic
, test_topic1 email
, test_topicSqs
]
-- -------------------------------------------------------------------------- --
-- Static Test parameters
--
-- TODO make these configurable
testProtocol :: Protocol
testProtocol = HTTP
-- | SQS endpoint
testSqsEndpoint :: SQS.Endpoint
testSqsEndpoint = SQS.sqsEndpointUsWest2
defaultTopicName :: T.Text
defaultTopicName = "test-topic"
defaultQueueName :: T.Text
defaultQueueName = "test-queue"
-- -------------------------------------------------------------------------- --
-- SNS Utils
snsConfiguration :: SnsConfiguration qt
snsConfiguration = SnsConfiguration testProtocol testRegion
simpleSns
:: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SnsConfiguration, MonadIO m)
=> r
-> m (MemoryResponse a)
simpleSns command = do
c <- baseConfiguration
simpleAws c snsConfiguration command
simpleSnsT
:: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SnsConfiguration, MonadIO m)
=> r
-> EitherT T.Text m (MemoryResponse a)
simpleSnsT = tryT . simpleSns
-- |
--
withTopic
:: T.Text -- ^ Topic name
-> (Arn -> IO a) -- ^ test function
-> IO a
withTopic topicName = bracket createTopic deleteTopic
where
createTopic = do
CreateTopicResponse arn <- simpleSns $ CreateTopic topicName
return arn
deleteTopic arn = simpleSns (DeleteTopic arn) >> return ()
withTopicTest
:: T.Text -- ^ Topic name
-> (IO Arn -> TestTree) -- ^ test tree
-> TestTree
withTopicTest topic = withResource createTopic deleteTopic
where
createTopic = do
CreateTopicResponse arn <- simpleSns $ CreateTopic tTopic
return arn
deleteTopic arn = void . simpleSns $ DeleteTopic arn
tTopic = testData topic
-- -------------------------------------------------------------------------- --
-- SQS Utils
sqsArn
:: Region
-> AccountId
-> T.Text -- ^ Queue Name
-> Arn
sqsArn region accountId queueName = Arn
{ arnService = ServiceNamespaceSqs
, arnRegion = Just region
, arnAccount = Just accountId
, arnResource = [queueName]
}
testSqsArn :: T.Text -> Arn
testSqsArn url = Arn
{ arnService = ServiceNamespaceSqs
, arnRegion = Just testRegion
, arnAccount = Just $ AccountId (sqsAccountIdText url)
, arnResource = [sqsQueueNameText url]
}
sqsQueueName :: T.Text -> SQS.QueueName
sqsQueueName url = SQS.QueueName (sqsQueueNameText url) (sqsAccountIdText url)
sqsQueueNameText :: T.Text -> T.Text
sqsQueueNameText url = T.split (== '/') url !! 4
sqsAccountIdText :: T.Text -> T.Text
sqsAccountIdText url = T.split (== '/') url !! 3
sqsConfiguration :: SQS.SqsConfiguration qt
sqsConfiguration = SQS.SqsConfiguration
{ SQS.sqsProtocol = testProtocol
, SQS.sqsEndpoint = testSqsEndpoint
, SQS.sqsPort = 80
, SQS.sqsUseUri = False
, SQS.sqsDefaultExpiry = 180
}
simpleSqs
:: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SQS.SqsConfiguration, MonadIO m)
=> r
-> m (MemoryResponse a)
simpleSqs command = do
c <- baseConfiguration
simpleAws c sqsConfiguration command
simpleSqsT
:: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SQS.SqsConfiguration, MonadIO m)
=> r
-> EitherT T.Text m (MemoryResponse a)
simpleSqsT = tryT . simpleSqs
withSqsQueue
:: T.Text
-- ^ queue name
-> (T.Text -> SQS.QueueName -> Arn -> IO a)
-- ^ the first argument is the queue URL,
-- the second is the 'SQS.QueueName', and
-- the third the queue 'Arn'.
-> IO a
withSqsQueue queueName f = bracket createQueue deleteQueue $ \url ->
f url (sqsQueueName url) (testSqsArn url)
where
createQueue = do
SQS.CreateQueueResponse url <- simpleSqs $ SQS.CreateQueue Nothing queueName
return url
deleteQueue url = void $ simpleSqs (SQS.DeleteQueue (sqsQueueName url))
withQueueTest
:: T.Text -- ^ Queue name
-> (IO (T.Text, SQS.QueueName, Arn) -> TestTree) -- ^ test tree
-> TestTree
withQueueTest queueName f = withResource createQueue deleteQueue $ \getQueueUrl ->
f $ do
url <- getQueueUrl
return (url, sqsQueueName url, testSqsArn url)
where
createQueue = do
SQS.CreateQueueResponse url <- simpleSqs $ SQS.CreateQueue Nothing queueName
return url
deleteQueue url = void $ simpleSqs (SQS.DeleteQueue (sqsQueueName url))
-- | Set queue policy attribute that allows an SNS Topic
-- to send notification to an SQS queue.
--
sqsAllowTopicAttribute
:: Arn -- ^ Queue ARN
-> T.Text -- ^ policy ID
-> Arn -- ^ topic ARN
-> SQS.SetQueueAttributes
sqsAllowTopicAttribute queueArn policyId topicArn = SQS.SetQueueAttributes
{ SQS.sqaAttribute = SQS.Policy
, SQS.sqaValue = T.decodeUtf8 . LB.toStrict . encode $ object
[ "Version" .= ("2012-10-17" :: T.Text)
, "Statement" .= [object
[ "Resource" .= queueArn
, "Sid" .= policyId
, "Effect" .= ("Allow" :: T.Text)
, "Principal" .= object [ "AWS" .= ("*" :: T.Text) ]
, "Action" .= ("sqs:SendMessage" :: T.Text)
, "Condition" .= object
[ "ArnEquals" .= object [ "aws:SourceArn" .= topicArn ]
]
]]
]
, SQS.sqaQueueName = queueId
}
where
queueId = SQS.QueueName
{ SQS.qAccountNumber = case arnAccount queueArn of
Nothing -> error $ "Malformed SQS queue ARN: " <> toText queueArn
Just (AccountId t) -> t
, SQS.qName = if length (arnResource queueArn) /= 1
then error $ "Malformed SQS queue ARN: " <> toText queueArn
else head $ arnResource queueArn
}
-- -------------------------------------------------------------------------- --
-- Topic Creation Tests
test_createTopic :: TestTree
test_createTopic = testGroup "Topic creation"
[ eitherTOnceTest1 "create list delete topic" prop_topicCreateListDelete
]
-- |
--
-- TODO:
--
-- * use 'awsIteratedList' for parsing the topics list
--
prop_topicCreateListDelete
:: T.Text -- ^ topic name
-> EitherT T.Text IO ()
prop_topicCreateListDelete topicName = do
CreateTopicResponse topicArn <- simpleSnsT $ CreateTopic tTopicName
handleT (\e -> deleteTopic topicArn >> left e) $ do
ListTopicsResponse _ allTopics <- simpleSnsT (ListTopics Nothing)
unless (topicArn `elem` allTopics)
. left $ "topic " <> toText topicArn <> " not listed"
deleteTopic topicArn
where
tTopicName = testData topicName
deleteTopic arn = void $ simpleSnsT (DeleteTopic arn)
-- -------------------------------------------------------------------------- --
-- Topic Tests
test_topic1
:: T.Text -- ^ email address
-> TestTree
test_topic1 email = withTopicTest defaultTopicName $ \getTopicArn ->
testGroup "Perform a series of tests on a single topic"
[ eitherTOnceTest0 "email subscribe"
$ liftIO getTopicArn >>= \t -> prop_emailSubscribe t email
]
-- | Subscribe an email endpoint (don't wait for confirmation).
--
prop_emailSubscribe
:: Arn -- ^ topic arn
-> SnsEndpoint -- ^ email addresss
-> EitherT T.Text IO ()
prop_emailSubscribe topicArn email = do
SubscribeResponse maybeSubArn <- simpleSnsT $ Subscribe (Just email) SnsProtocolEmail topicArn
case maybeSubArn of
Nothing -> return ()
Just subArn -> do
let e = "unexpected subscription arn when 'confirmation pending' is expected"
void . handleT (\e2 -> left (e <> " and " <> e2))
. simpleSnsT $ Unsubscribe subArn
left e
-- -------------------------------------------------------------------------- --
-- SQS Integration Tests
test_topicSqs :: TestTree
test_topicSqs = withTopicTest defaultTopicName $ \getTopicArn ->
withQueueTest defaultQueueName $ \getQueueParams -> testGroup "SQS Integration Tests"
[ eitherTOnceTest0 "SQS subscribe publish unsubscribe" $ do
topicArn <- liftIO getTopicArn
(_, queueId, queueArn) <- liftIO getQueueParams
prop_sqsSubscribePublishUnsubscribe topicArn queueId queueArn
]
-- | Subscribe an SQS queue to an SNS topic
--
prop_sqsSubscribePublishUnsubscribe
:: Arn -- ^ topic arn
-> SQS.QueueName -- ^ queue id
-> Arn -- queue Arn
-> EitherT T.Text IO ()
prop_sqsSubscribePublishUnsubscribe topicArn queueId queueArn = do
-- Add permission to send messages from SNS topic (identified by ARN) to queue
void . simpleSqsT $ sqsAllowTopicAttribute queueArn sqsPermissionId topicArn
-- subscribe Queue to SNS topci
SubscribeResponse maybeSubArn <- simpleSnsT $ Subscribe (Just $ toText queueArn) SnsProtocolSqs topicArn
subArn <- maybeSubArn ?? "Subscription failed: subscription Arn is missing probably because the confirmation is yet pending"
-- Let's wait 5 seconds, just be on the safe side ...
liftIO $ threadDelay (5*1000000)
-- publish to topic
PublishResponse msgId0 <- simpleSnsT $ Publish msg Nothing (Just subj) (Left topicArn)
-- receive messages
#if MIN_VERSION_aws(0,10,0)
let numRetry = 3
#else
let numRetry = 6
#endif
sqsMsg <- retryT numRetry $ do
SQS.ReceiveMessageResponse msgs <- simpleSqsT $ SQS.ReceiveMessage
{ SQS.rmVisibilityTimeout = Nothing
, SQS.rmAttributes = []
, SQS.rmMaxNumberOfMessages = Just 1
, SQS.rmQueueName = queueId
#if MIN_VERSION_aws(0,10,0)
, SQS.rmUserMessageAttributes = []
, SQS.rmWaitTimeSeconds = Just 20
#endif
}
when (length msgs < 1) $ left
$ "unexpected number of messages in queue; expected 1, got " <> sshow (length msgs)
return $ head msgs
-- parse notification
notification :: SqsNotification <- fmapLT T.pack . hoistEither
. eitherDecode . LB.fromStrict . T.encodeUtf8 $ SQS.mBody sqsMsg
-- check result
when (sqsNotificationMessageId notification /= msgId0) $ left
$ "message IDs don't match; epxected " <> q (messageIdText msgId0)
<> ", got " <> q (messageIdText $ sqsNotificationMessageId notification)
when (sqsNotificationMessage notification /= snsMessageDefault msg) $ left
$ "messages don't match; expected " <> q (snsMessageDefault msg)
<> ", got " <> q (sqsNotificationMessage notification)
-- unsubscribe queue
void $ simpleSnsT $ Unsubscribe subArn
where
q t = "\"" <> t <> "\""
sqsPermissionId = testData . head . arnResource $ topicArn
msg = snsMessage "message abc"
subj = "subject abc"