Skip to content

Commit 8390a56

Browse files
authored
Merge pull request #32 from ambarltd/transport-tests
Add Transport tests
2 parents 7b53e56 + 9e0a762 commit 8390a56

File tree

4 files changed

+150
-1
lines changed

4 files changed

+150
-1
lines changed

emulator.cabal

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ test-suite emulator-tests
116116
Test.Connector.PostgreSQL
117117
Test.Emulator
118118
Test.OnDemand
119+
Test.Transport
119120
Test.Warden
120121
Test.Utils.OnDemand
121122
Test.Utils.Docker
@@ -133,6 +134,7 @@ test-suite emulator-tests
133134
, filepath
134135
, hspec
135136
, hspec-expectations
137+
, http-types
136138
, HUnit
137139
, postgresql-simple
138140
, process
@@ -144,6 +146,9 @@ test-suite emulator-tests
144146
, time
145147
, text
146148
, unordered-containers
149+
, wai
150+
, wai-extra
151+
, warp
147152

148153
benchmark emulator-bench
149154
import: common

src/Ambar/Transport/Http.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ instance Transport HttpTransport where
6262
req = base
6363
{ Http.requestBody = Http.RequestBodyBS bs
6464
, Http.method = "POST"
65-
, Http.requestHeaders = [("Content-Type", "application/json")]
65+
, Http.requestHeaders =
66+
[("Content-Type", "application/json")] <> Http.requestHeaders base
6667
}
6768

6869
decode :: ByteString -> Maybe SubmissionError

tests/Test/Transport.hs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
module Test.Transport
2+
( testTransport
3+
) where
4+
5+
import Control.Concurrent (MVar, Chan, newMVar, modifyMVar, newChan, writeChan, readChan)
6+
import Control.Monad (replicateM, forM_)
7+
import Control.Exception (throwIO, ErrorCall(..))
8+
import Data.Aeson (ToJSON, FromJSON)
9+
import qualified Data.Aeson as Json
10+
import Data.ByteString.Lazy (ByteString)
11+
import qualified Data.ByteString.Lazy as LB
12+
import qualified Data.Text as Text
13+
import qualified Data.Text.Encoding as Text
14+
import Data.Time.Clock.POSIX (getPOSIXTime)
15+
import Network.Wai (Request)
16+
import GHC.Stack (HasCallStack)
17+
import qualified Network.Wai as Wai
18+
import Network.Wai.Handler.Warp (Port)
19+
import Network.HTTP.Types (Status(..))
20+
import qualified Network.Wai.Handler.Warp as Warp
21+
import qualified Network.Wai.Middleware.HttpAuth as Wai
22+
import System.IO.Unsafe (unsafePerformIO)
23+
import Test.Hspec
24+
( Spec
25+
, it
26+
, describe
27+
, shouldBe
28+
)
29+
30+
import qualified Ambar.Transport.Http as HttpTransport
31+
import Ambar.Transport (Transport(..))
32+
import Ambar.Transport.Http (Endpoint(..), User(..), Password(..))
33+
34+
import Utils.Async (withAsyncThrow)
35+
import Utils.Some (Some(..))
36+
37+
testTransport :: Spec
38+
testTransport = describe "transport" $ do
39+
testHttpTransport
40+
41+
testHttpTransport :: HasCallStack => Spec
42+
testHttpTransport = describe "Http" $
43+
it "sends authenticated requests" $
44+
withHttpTransport $ \transport getResult -> do
45+
let submit :: ToJSON a => a -> IO ()
46+
submit val = do
47+
r <- sendJSON transport (LB.toStrict $ Json.encode val)
48+
forM_ r $ \err -> throwIO $ ErrorCall $ show err
49+
50+
receive :: FromJSON a => IO a
51+
receive = do
52+
r <- getResult
53+
case Json.eitherDecode' r of
54+
Left err -> throwIO $ ErrorCall $
55+
"Error decoding received message: " <> err <> "\n" <> show r
56+
Right v -> return v
57+
58+
entries = [("A", 1), ("B", 2), ("C", 3)] :: [(String, Int)]
59+
60+
forM_ entries submit
61+
received <- replicateM (length entries) receive
62+
received `shouldBe` entries
63+
64+
data Creds = Creds User Password
65+
66+
withHttpTransport :: (Some Transport -> IO ByteString -> IO a) -> IO a
67+
withHttpTransport act =
68+
withHttpServer $ \(Creds user pass) endpoint getResponseBody -> do
69+
transport <- HttpTransport.new endpoint user pass
70+
act (Some transport) getResponseBody
71+
72+
{-# NOINLINE lastPort #-}
73+
lastPort :: MVar Port
74+
lastPort = unsafePerformIO (newMVar 49152)
75+
76+
nextPort :: IO Port
77+
nextPort = modifyMVar lastPort $ \n -> return (n + 1, n)
78+
79+
withHttpServer :: (Creds -> Endpoint -> IO ByteString -> IO a) -> IO a
80+
withHttpServer act = do
81+
t <- getPOSIXTime
82+
port <- nextPort
83+
chan <- newChan
84+
let user = User $ "user_" <> Text.pack (show t)
85+
pass = Password $ "pass_" <> Text.pack (show t)
86+
creds = Creds user pass
87+
readResponse = readChan chan
88+
endpoint = Endpoint $ Text.pack $ "http://localhost:" <> show port <> "/endpoint"
89+
withAsyncThrow (server chan creds port) $
90+
act creds endpoint readResponse
91+
where
92+
server :: Chan ByteString -> Creds -> Port -> IO ()
93+
server chan creds port = Warp.run port $ withAuth creds $ requestHandler chan
94+
95+
withAuth :: Creds -> Wai.Application -> Wai.Application
96+
withAuth creds f = Wai.basicAuth checkCreds authSettings f
97+
where
98+
authSettings = "realm" :: Wai.AuthSettings
99+
Creds (User user) (Password pass) = creds
100+
checkCreds bsuser bspass = return $
101+
Text.decodeUtf8 bsuser == user &&
102+
Text.decodeUtf8 bspass == pass
103+
104+
requestHandler
105+
:: Chan ByteString
106+
-> Request
107+
-> (Wai.Response -> IO Wai.ResponseReceived)
108+
-> IO Wai.ResponseReceived
109+
requestHandler chan req respond = do
110+
response <- case toResponse req of
111+
Left (status, msg) ->
112+
return $ Wai.responseLBS status [] msg
113+
Right (status, msg) -> do
114+
body <- Wai.consumeRequestBodyStrict req
115+
writeChan chan body
116+
return $ Wai.responseLBS status [] msg
117+
respond response
118+
119+
toResponse :: Request -> Either (Status, ByteString) (Status, ByteString)
120+
toResponse req = do
121+
methodIs "POST"
122+
pathIs ["endpoint"]
123+
return (Status 200 "Success", "{ \"result\": { \"success\" : {} } }")
124+
where
125+
asBS = LB.fromStrict . Text.encodeUtf8
126+
methodIs m = do
127+
let method = Text.decodeUtf8 $ Wai.requestMethod req
128+
if method == m
129+
then return ()
130+
else Left (Status 403 "Invalid Method", asBS $ "Expected " <> m <> " but got " <> method)
131+
132+
pathIs p = do
133+
let path = Wai.pathInfo req
134+
if path == p
135+
then return ()
136+
else Left
137+
(Status 404 "Unknown Endpoint", asBS $ "Unknown endpoint. Only valid path is '/" <> Text.intercalate "/" p <> "'")
138+
139+
140+
141+

tests/Tests.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Test.Hspec (hspec, parallel)
44

55
import Test.Config (testConfig)
66
import Test.Emulator (testEmulator)
7+
import Test.Transport (testTransport)
78
import Test.Queue (testQueues)
89
import Test.Connector (testConnectors, withDatabases, Databases(..))
910
import Test.OnDemand (testOnDemand)
@@ -27,5 +28,6 @@ main =
2728
testWarden
2829
testConfig
2930
testQueues
31+
testTransport
3032
testEmulator pcreds
3133
testConnectors dbs

0 commit comments

Comments
 (0)