Skip to content

Commit 2e2c89e

Browse files
authored
[TSD-85] Added basic tests, missing tests on other calls on DataLayer. (#57)
* [TSD-85] Added basic tests, missing tests on other calls on DataLayer. * [TSD-85] Added more exceptions to track, arbitrary exception tests. * [TSD-85] Added http status exceptions, now we can track different exceptions from this layer. * [TSD-85] Fixed catching a single type of exception that is acceptable, rethrow others. * [TSD-85] Fixed getTicketComments function to return an empty list if the ticket cannot be found.
1 parent af6bd53 commit 2e2c89e

File tree

7 files changed

+267
-75
lines changed

7 files changed

+267
-75
lines changed

log-classifier.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ library
2828
DataSource
2929
Configuration
3030
Util
31+
HttpLayer
3132
other-modules: Paths_log_classifier
3233
DataSource.DB
3334
DataSource.Types
3435
DataSource.Http
35-
HttpLayer
3636
ghc-options: -Wall
3737
build-depends: aeson
3838
, array

src/DataSource/DB.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ emptyDBLayer = DBLayer
123123

124124
-- | The simple connection Zendesk layer. Used for database querying.
125125
-- We need to sync occasionaly.
126-
connDataLayer :: forall m. (MonadIO m, MonadReader Config m) => DataLayer m
126+
connDataLayer :: forall m. (MonadIO m, MonadCatch m, MonadReader Config m) => DataLayer m
127127
connDataLayer = DataLayer
128128
{ zlGetTicketInfo = \tId -> withProdDatabase $ \conn -> getTicketInfoByTicketId conn tId
129129
, zlListDeletedTickets = zlListDeletedTickets basicDataLayer
@@ -158,7 +158,7 @@ connDBLayer = DBLayer
158158
-- | The connection pooled Zendesk layer. Used for database querying.
159159
-- We need to sync occasionaly.
160160
connPoolDataLayer
161-
:: forall m. (MonadBaseControl IO m, MonadIO m, MonadReader Config m)
161+
:: forall m. (MonadBaseControl IO m, MonadIO m, MonadCatch m, MonadReader Config m)
162162
=> DBConnPool
163163
-> DataLayer m
164164
connPoolDataLayer connPool = DataLayer
@@ -176,7 +176,7 @@ connPoolDataLayer connPool = DataLayer
176176

177177

178178
-- | The connection pooled database layer. Used for database modification.
179-
connPoolDBLayer
179+
connPoolDBLayer
180180
:: forall m. ( MonadBaseControl IO m, MonadIO m, MonadReader Config m, MonadCatch m)
181181
=> DBConnPool
182182
-> DBLayer m

src/DataSource/Http.hs

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import Data.Aeson.Text (encodeToLazyText)
1818
import Data.List (nub)
1919
import Network.HTTP.Simple (Request, getResponseBody, httpLBS, parseRequest_)
2020

21-
import HttpLayer (HTTPNetworkLayer (..), apiRequest, apiRequestAbsolute)
21+
import HttpLayer (HTTPNetworkLayer (..), HttpNetworkLayerException (..), apiRequest,
22+
apiRequestAbsolute)
2223

2324
import DataSource.Types (Attachment (..), AttachmentContent (..), Comment (..),
2425
CommentBody (..), CommentId (..), Config (..), DataLayer (..),
@@ -39,7 +40,7 @@ import DataSource.Types (Attachment (..), AttachmentContent (..), Comm
3940
-- - get returns a single result (wrapped in @Maybe@)
4041
-- - list returns multiple results
4142
-- - post submits a result (maybe PUT?!)
42-
basicDataLayer :: (MonadIO m, MonadReader Config m) => DataLayer m
43+
basicDataLayer :: (MonadIO m, MonadCatch m, MonadReader Config m) => DataLayer m
4344
basicDataLayer = DataLayer
4445
{ zlGetTicketInfo = getTicketInfo
4546
, zlListDeletedTickets = listDeletedTickets
@@ -74,7 +75,7 @@ emptyDataLayer = DataLayer
7475

7576
-- | Get single ticket info.
7677
getTicketInfo
77-
:: (HasCallStack, MonadIO m, MonadReader Config m)
78+
:: (HasCallStack, MonadIO m, MonadCatch m, MonadReader Config m)
7879
=> TicketId
7980
-> m (Maybe TicketInfo)
8081
getTicketInfo ticketId = do
@@ -85,7 +86,54 @@ getTicketInfo ticketId = do
8586

8687
apiCall <- asksHTTPNetworkLayer hnlApiCall
8788

88-
Just <$> apiCall parseTicket req
89+
result <- try $ apiCall parseTicket req
90+
91+
notFoundExceptionToMaybe result
92+
where
93+
-- | We want only @HttpNotFound@ to return @Nothing@, otherwise
94+
-- propagate the exception.
95+
notFoundExceptionToMaybe
96+
:: forall m a. (MonadCatch m)
97+
=> Either HttpNetworkLayerException a
98+
-> m (Maybe a)
99+
notFoundExceptionToMaybe result = case result of
100+
Left exception -> case exception of
101+
HttpNotFound _ -> pure Nothing
102+
otherExceptions -> throwM otherExceptions
103+
Right ticketInfo -> pure $ Just ticketInfo
104+
105+
-- | Get ticket's comments
106+
getTicketComments
107+
:: (HasCallStack, MonadIO m, MonadCatch m, MonadReader Config m)
108+
=> TicketId
109+
-> m [Comment]
110+
getTicketComments tId = do
111+
cfg <- ask
112+
113+
apiCall <- asksHTTPNetworkLayer hnlApiCall
114+
115+
let url = showURL $ TicketCommentsURL tId
116+
let req = apiRequest cfg url
117+
118+
result <- try $ apiCall parseComments req
119+
120+
notFoundExceptionToList result
121+
where
122+
-- | We want only @HttpNotFound@ to return an empty list, otherwise
123+
-- propagate the exception.
124+
-- When a @Ticket@ is not found, we don't have any of the @[Comment]@,
125+
-- effectivly returning an empty list.
126+
-- We might include a stricter policy to cause exception when no ticket is
127+
-- found, but this should be good as well.
128+
notFoundExceptionToList
129+
:: forall m a. (MonadCatch m)
130+
=> Either HttpNetworkLayerException [a]
131+
-> m [a]
132+
notFoundExceptionToList result = case result of
133+
Left exception -> case exception of
134+
HttpNotFound _ -> pure []
135+
otherExceptions -> throwM otherExceptions
136+
Right comments -> pure comments
89137

90138
-- | Return list of deleted tickets.
91139
listDeletedTickets
@@ -151,7 +199,7 @@ listAdminAgents = do
151199
-- NOTE: If count is less than 1000, then stop paginating.
152200
-- Otherwise, use the next_page URL to get the next page of results.
153201
getExportedTickets
154-
:: forall m. (MonadIO m, MonadReader Config m)
202+
:: forall m. (MonadIO m, MonadCatch m, MonadReader Config m)
155203
=> ExportFromTime
156204
-> m [TicketInfo]
157205
getExportedTickets time = do
@@ -193,7 +241,7 @@ getExportedTickets time = do
193241

194242
-- | Send API request to post comment
195243
postTicketComment
196-
:: (MonadIO m, MonadReader Config m)
244+
:: (MonadIO m, MonadCatch m, MonadReader Config m)
197245
=> TicketInfo
198246
-> ZendeskResponse
199247
-> m ()
@@ -231,7 +279,7 @@ createResponseTicket agentId TicketInfo{..} ZendeskResponse{..} =
231279

232280
-- | Get user information.
233281
_getUser
234-
:: (MonadIO m, MonadReader Config m)
282+
:: (MonadIO m, MonadCatch m, MonadReader Config m)
235283
=> m User
236284
_getUser = do
237285
cfg <- ask
@@ -253,27 +301,6 @@ getAttachment Attachment{..} = Just . AttachmentContent . getResponseBody <$> ht
253301
req :: Request
254302
req = parseRequest_ (toString aURL)
255303

256-
-- | Get ticket's comments
257-
getTicketComments
258-
:: (MonadIO m, MonadReader Config m)
259-
=> TicketId
260-
-> m [Comment]
261-
getTicketComments tId = do
262-
cfg <- ask
263-
264-
apiCallSafe <- asksHTTPNetworkLayer hnlApiCallSafe
265-
266-
let url = showURL $ TicketCommentsURL tId
267-
let req = apiRequest cfg url
268-
269-
result <- apiCallSafe parseComments req
270-
271-
-- TODO(ks): For now return empty if there is an exception.
272-
-- After we have exception handling, we propagate this up.
273-
case result of
274-
Left _ -> pure []
275-
Right r -> pure r
276-
277304
------------------------------------------------------------
278305
-- Utility
279306
------------------------------------------------------------

src/DataSource/Types.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,10 @@ data DataLayer m = DataLayer
206206
-- We need to use RankNTypes due to some complications that appeared.
207207
data HTTPNetworkLayer = HTTPNetworkLayer
208208
{ hnlAddJsonBody :: forall a. (ToJSON a) => a -> Request -> Request
209-
-- TODO(ks): These two below are basically the same, will fix in future PR, requires refactoring.
210-
, hnlApiCall :: forall m a. (MonadIO m, FromJSON a) => (Value -> Parser a) -> Request -> m a
211-
, hnlApiCallSafe :: forall m a. (MonadIO m, FromJSON a) => (Value -> Parser a) -> Request -> m (Either String a)
209+
, hnlApiCall :: forall m a. (MonadIO m, MonadCatch m, FromJSON a)
210+
=> (Value -> Parser a)
211+
-> Request
212+
-> m a
212213
}
213214

214215
-- | The IOLayer interface that we can expose.

src/HttpLayer.hs

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module HttpLayer
22
( HTTPNetworkLayer (..)
3+
, HttpNetworkLayerException (..)
34
, basicHTTPNetworkLayer
45
, emptyHTTPNetworkLayer
56
, apiRequest
@@ -8,14 +9,16 @@ module HttpLayer
89

910
import Universum
1011

11-
import Data.Aeson (FromJSON, ToJSON, Value, encode)
12+
import Data.Aeson (FromJSON, ToJSON, Value)
1213
import Data.Aeson.Types (Parser, parseEither)
13-
import Network.HTTP.Simple (Request, addRequestHeader, getResponseBody, httpJSON,
14-
parseRequest_, setRequestBasicAuth, setRequestBodyJSON,
15-
setRequestMethod, setRequestPath)
14+
import Network.HTTP.Simple (Request, addRequestHeader, defaultRequest, getResponseBody,
15+
getResponseStatusCode, httpJSONEither, parseRequest_,
16+
setRequestBasicAuth, setRequestBodyJSON, setRequestMethod,
17+
setRequestPath)
1618

1719
import DataSource.Types (Config (..), HTTPNetworkLayer (..))
1820

21+
import Test.QuickCheck (Arbitrary (..), oneof)
1922

2023
------------------------------------------------------------
2124
-- Layer
@@ -25,16 +28,75 @@ basicHTTPNetworkLayer :: HTTPNetworkLayer
2528
basicHTTPNetworkLayer = HTTPNetworkLayer
2629
{ hnlAddJsonBody = addJsonBody
2730
, hnlApiCall = apiCall
28-
, hnlApiCallSafe = apiCallSafe
2931
}
3032

3133
emptyHTTPNetworkLayer :: HTTPNetworkLayer
3234
emptyHTTPNetworkLayer = HTTPNetworkLayer
3335
{ hnlAddJsonBody = \_ _ -> error "hnlAddJsonBody not implemented!"
3436
, hnlApiCall = \_ -> error "hnlApiCall not implemented!"
35-
, hnlApiCallSafe = \_ _ -> error "hnlApiCallSafe not implemented!"
3637
}
3738

39+
------------------------------------------------------------
40+
-- Exceptions
41+
------------------------------------------------------------
42+
43+
data HttpNetworkLayerException
44+
= HttpDecodingJSON Request String
45+
-- 4XX
46+
| HttpBadRequest Request
47+
| HttpUnauthorized Request
48+
| HttpForbidden Request
49+
| HttpNotFound Request
50+
| HttpMethodNotAllowed Request
51+
| HttpUnsupportedMediaType Request
52+
| HttpTooManyRequests Request
53+
-- 5XX
54+
| HttpInternalServerError Request
55+
| HttpNotImplemented Request
56+
| HttpServiceUnavailable Request
57+
deriving (Show)
58+
59+
-- | The way to convert the status codes to exceptions.
60+
statusCodeToException :: forall m. (MonadThrow m) => Request -> Int -> m ()
61+
statusCodeToException request = \case
62+
400 -> throwM $ HttpBadRequest request
63+
401 -> throwM $ HttpUnauthorized request
64+
403 -> throwM $ HttpForbidden request
65+
404 -> throwM $ HttpNotFound request
66+
405 -> throwM $ HttpMethodNotAllowed request
67+
415 -> throwM $ HttpUnsupportedMediaType request
68+
429 -> throwM $ HttpTooManyRequests request
69+
70+
500 -> throwM $ HttpInternalServerError request
71+
501 -> throwM $ HttpNotImplemented request
72+
503 -> throwM $ HttpServiceUnavailable request
73+
74+
_ -> pure ()
75+
76+
77+
instance Exception HttpNetworkLayerException
78+
79+
-- | TODO(ks): Good enough for our purposes
80+
instance Arbitrary Request where
81+
arbitrary = pure defaultRequest
82+
83+
instance Arbitrary HttpNetworkLayerException where
84+
arbitrary = oneof
85+
[ HttpDecodingJSON <$> arbitrary <*> arbitrary
86+
87+
, HttpBadRequest <$> arbitrary
88+
, HttpUnauthorized <$> arbitrary
89+
, HttpForbidden <$> arbitrary
90+
, HttpNotFound <$> arbitrary
91+
, HttpMethodNotAllowed <$> arbitrary
92+
, HttpUnsupportedMediaType <$> arbitrary
93+
, HttpTooManyRequests <$> arbitrary
94+
95+
, HttpInternalServerError <$> arbitrary
96+
, HttpNotImplemented <$> arbitrary
97+
, HttpServiceUnavailable <$> arbitrary
98+
]
99+
38100
------------------------------------------------------------
39101
-- Functions
40102
------------------------------------------------------------
@@ -64,29 +126,27 @@ addJsonBody :: forall a. ToJSON a => a -> Request -> Request
64126
addJsonBody body req = setRequestBodyJSON body $ setRequestMethod "PUT" req
65127

66128
-- | Make an api call
67-
-- TODO(ks): Switch to @Either@.
68129
apiCall
69-
:: forall m a. (MonadIO m, FromJSON a)
130+
:: forall m a. (MonadIO m, MonadCatch m, FromJSON a)
70131
=> (Value -> Parser a)
71132
-> Request
72133
-> m a
73-
apiCall parser req = do
74-
putTextLn $ show req
75-
v <- getResponseBody <$> httpJSON req
76-
case parseEither parser v of
77-
Right o -> pure o
78-
Left e -> error $ "couldn't parse response "
79-
<> toText e <> "\n" <> decodeUtf8 (encode v)
80-
81-
-- | Make a safe api call.
82-
apiCallSafe
83-
:: forall m a. (MonadIO m, FromJSON a)
84-
=> (Value -> Parser a)
85-
-> Request
86-
-> m (Either String a)
87-
apiCallSafe parser req = do
88-
putTextLn $ show req
89-
v <- getResponseBody <$> httpJSON req
90-
pure $ parseEither parser v
134+
apiCall parser request = do
135+
-- putTextLn $ show req -- logging !?!
136+
httpResult <- httpJSONEither request
137+
138+
let httpResponseBody = getResponseBody httpResult
139+
140+
httpResponse <- either
141+
(throwM . HttpDecodingJSON request . show)
142+
pure
143+
httpResponseBody
144+
145+
let httpStatusCode = getResponseStatusCode httpResult
146+
147+
_ <- statusCodeToException request httpStatusCode
91148

149+
case parseEither parser httpResponse of
150+
Left reason -> throwM $ HttpDecodingJSON request reason
151+
Right value -> pure value
92152

src/Lib.hs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ module Lib
1414
, listAndSortTickets
1515
, filterAnalyzedTickets
1616
, exportZendeskDataToLocalDB
17+
-- * optional
18+
, fetchTicket
19+
, fetchTicketComments
1720
) where
1821

1922
import Universum
@@ -354,6 +357,20 @@ fetchTickets = do
354357
-- Anything that has a "to_be_analysed" tag
355358
pure $ filter (elem (renderTicketStatus ToBeAnalyzed) . getTicketTags . tiTags) allTickets
356359

360+
-- | Fetch a single ticket if found.
361+
fetchTicket :: TicketId -> App (Maybe TicketInfo)
362+
fetchTicket ticketId = do
363+
getTicketInfo <- asksDataLayer zlGetTicketInfo
364+
getTicketInfo ticketId
365+
366+
367+
-- | Fetch comments from a ticket, if the ticket is found.
368+
fetchTicketComments :: TicketId -> App [Comment]
369+
fetchTicketComments ticketId = do
370+
getTicketComments <- asksDataLayer zlGetTicketComments
371+
getTicketComments ticketId
372+
373+
357374
fetchAndShowTickets :: App ()
358375
fetchAndShowTickets = do
359376
allTickets <- fetchTickets

0 commit comments

Comments
 (0)