@@ -1,29 +1,33 @@
11{-# LANGUAGE CPP #-}
22module Servant.QuickCheck.InternalSpec (spec) where
33
4+
45import Control.Concurrent.MVar (newMVar, readMVar, swapMVar)
6+import Control.Exception (SomeException)
7+import Control.Monad (replicateM)
58import Control.Monad.IO.Class (liftIO)
69import qualified Data.ByteString as BS
710import qualified Data.ByteString.Char8 as C
11+import Data.Maybe (fromJust)
12+import Network.HTTP.Client (path, queryString)
813import Prelude.Compat
914import Servant
15+import Servant.HTML.Blaze (HTML)
16+import qualified Text.Blaze.Html as Blaze
17+import qualified Text.Blaze.Html5 as Blaze5
1018import Test.Hspec (Spec, context, describe, it, shouldBe,
1119 shouldContain)
12−import Test.Hspec.Core.Spec (Arg, Example, Result (..),
13− defaultParams, evaluateExample)
14−import Test.QuickCheck.Gen (unGen)
20+import Test.Hspec.Core.Spec (Arg, Example, Result (..), ResultStatus (..),
21+ defaultParams, safeEvaluateExample)
22+import Test.QuickCheck.Gen (generate, unGen)
1523import Test.QuickCheck.Random (mkQCGen)
16−import Network.HTTP.Client (queryString)
1724
18−#if MIN_VERSION_servant(0,8,0)
19−import Servant.API.Internal.Test.ComprehensiveAPI (comprehensiveAPIWithoutRaw)
20−#else
21−import Servant.API.Internal.Test.ComprehensiveAPI (ComprehensiveAPI,
22− comprehensiveAPI)
23−#endif
25+
26+import Servant.Test.ComprehensiveAPI (comprehensiveAPIWithoutStreamingOrRaw)
2427
2528import Servant.QuickCheck
26−import Servant.QuickCheck.Internal (genRequest, serverDoesntSatisfy)
29+import Servant.QuickCheck.Internal (genRequest, runGenRequest,
30+ serverDoesntSatisfy)
2731
2832spec :: Spec
2933spec = do
@@ -33,6 +37,10 @@ spec = do
3337 onlyJsonObjectSpec
3438 notLongerThanSpec
3539 queryParamsSpec
40+ queryFlagsSpec
41+ deepPathSpec
42+ htmlDocTypesSpec
43+ unbiasedGenerationSpec
3644
3745serversEqualSpec :: Spec
3846serversEqualSpec = describe "serversEqual" $ do
@@ -43,15 +51,42 @@ serversEqualSpec = describe "serversEqual" $ do
4351 serversEqual api burl1 burl2 args bodyEquality
4452
4553 context "when servers are not equal" $ do
46−
47−
4854 it "provides the failing responses in the error message" $ do
49− Fail _ err <- withServantServer api2 server2 $ \burl1 ->
55+ FailedWith err <- withServantServer api2 server2 $ \burl1 ->
5056 withServantServer api2 server3 $ \burl2 -> do
5157 evalExample $ serversEqual api2 burl1 burl2 args bodyEquality
58+ show err `shouldContain` "Server equality failed"
5259 show err `shouldContain` "Body: 1"
5360 show err `shouldContain` "Body: 2"
54− show err `shouldContain` "Path: failplz/"
61+ show err `shouldContain` "Path: /failplz"
62+
63+ context "when JSON is equal but looks a bit different as a ByteString" $ do
64+
65+ it "sanity check: different whitespace same JSON objects bodyEquality fails" $ do
66+ FailedWith err <- withServantServer jsonApi jsonServer1 $ \burl1 ->
67+ withServantServer jsonApi jsonServer2 $ \burl2 -> do
68+ evalExample $ serversEqual jsonApi burl1 burl2 args bodyEquality
69+ show err `shouldContain` "Server equality failed"
70+
71+ it "jsonEquality considers equal JSON apis equal regardless of key ordering or whitespace" $ do
72+ withServantServerAndContext jsonApi ctx jsonServer1 $ \burl1 ->
73+ withServantServerAndContext jsonApi ctx jsonServer2 $ \burl2 ->
74+ serversEqual jsonApi burl1 burl2 args jsonEquality
75+
76+ it "sees when JSON apis are not equal because any value is different" $ do
77+ FailedWith err <- withServantServer jsonApi jsonServer2 $ \burl1 ->
78+ withServantServer jsonApi jsonServer3 $ \burl2 -> do
79+ evalExample $ serversEqual jsonApi burl1 burl2 args jsonEquality
80+ show err `shouldContain` "Server equality failed"
81+ show err `shouldContain` "Path: /jsonComparison"
82+
83+ it "sees when JSON apis are not equal due to different keys but same values" $ do
84+ FailedWith err <- withServantServer jsonApi jsonServer2 $ \burl1 ->
85+ withServantServer jsonApi jsonServer4 $ \burl2 -> do
86+ evalExample $ serversEqual jsonApi burl1 burl2 args jsonEquality
87+ show err `shouldContain` "Server equality failed"
88+ show err `shouldContain` "Path: /jsonComparison"
89+
5590
5691serverSatisfiesSpec :: Spec
5792serverSatisfiesSpec = describe "serverSatisfies" $ do
@@ -73,33 +108,38 @@ serverSatisfiesSpec = describe "serverSatisfies" $ do
73108 context "when predicates are false" $ do
74109
75110 it "fails with informative error messages" $ do
76− Fail _ err <- withServantServerAndContext api ctx server $ \burl -> do
77− evalExample $ serverSatisfies api burl args (getsHaveCacheControlHeader <%> mempty)
78− err `shouldContain` "getsHaveCacheControlHeader"
79− err `shouldContain` "Headers"
80− err `shouldContain` "Body"
111+ FailedWith err <- withServantServerAndContext api ctx server $ \burl -> do
112+ evalExample $ serverSatisfies api burl args (notAllowedContainsAllowHeader <%> mempty)
113+ show err `shouldContain` "notAllowedContainsAllowHeader"
114+ show err `shouldContain` "Headers"
115+ show err `shouldContain` "Body"
116+
81117
82118onlyJsonObjectSpec :: Spec
83119onlyJsonObjectSpec = describe "onlyJsonObjects" $ do
84120
85121 it "fails correctly" $ do
86− Fail _ err <- withServantServerAndContext api ctx server $ \burl -> do
122+ FailedWith err <- withServantServerAndContext api ctx server $ \burl -> do
87123 evalExample $ serverSatisfies (Proxy :: Proxy (Get '[JSON] Int)) burl args
88124 (onlyJsonObjects <%> mempty)
89− err `shouldContain` "onlyJsonObjects"
125+ show err `shouldContain` "onlyJsonObjects"
90126
91127 it "accepts non-JSON endpoints" $ do
92128 withServantServerAndContext octetAPI ctx serverOctetAPI $ \burl ->
93129 serverSatisfies octetAPI burl args (onlyJsonObjects <%> mempty)
94130
131+ it "does not fail when there is no content-type" $ do
132+ withServantServerAndContext api2 ctx serverFailing $ \burl ->
133+ serverSatisfies api2 burl args (onlyJsonObjects <%> mempty)
134+
95135notLongerThanSpec :: Spec
96136notLongerThanSpec = describe "notLongerThan" $ do
97137
98138 it "fails correctly" $ do
99− Fail _ err <- withServantServerAndContext api ctx server $ \burl -> do
139+ FailedWith err <- withServantServerAndContext api ctx server $ \burl -> do
100140 evalExample $ serverSatisfies (Proxy :: Proxy (Get '[JSON] Int)) burl args
101141 (notLongerThan 1 <%> mempty)
102− err `shouldContain` "notLongerThan"
142+ show err `shouldContain` "notLongerThan"
103143
104144 it "succeeds correctly" $ do
105145 withServantServerAndContext api ctx server $ \burl ->
@@ -109,19 +149,82 @@ isComprehensiveSpec :: Spec
109149isComprehensiveSpec = describe "HasGenRequest" $ do
110150
111151 it "has instances for all 'servant' combinators" $ do
112− let _g = genRequest comprehensiveAPIWithoutRaw
152+ let _g = genRequest comprehensiveAPIWithoutStreamingOrRaw
113153 True `shouldBe` True -- This is a type-level check
114154
155+deepPathSpec :: Spec
156+deepPathSpec = describe "Path components" $ do
157+
158+ it "are separated by slashes, without a trailing slash" $ do
159+ let rng = mkQCGen 0
160+ burl = BaseUrl Http "localhost" 80 ""
161+ gen = runGenRequest deepAPI
162+ req = (unGen gen rng 0) burl
163+ path req `shouldBe` ("/one/two/three")
164+
165+
115166queryParamsSpec :: Spec
116167queryParamsSpec = describe "QueryParams" $ do
117168
118169 it "reduce to an HTTP query string correctly" $ do
119170 let rng = mkQCGen 0
120171 burl = BaseUrl Http "localhost" 80 ""
121− gen = genRequest paramsAPI
172+ gen = runGenRequest paramsAPI
173+ req = (unGen gen rng 0) burl
174+ qs = C.unpack $ queryString req
175+ qs `shouldBe` "one=_&two=_"
176+
177+queryFlagsSpec :: Spec
178+queryFlagsSpec = describe "QueryFlags" $ do
179+
180+ it "reduce to an HTTP query string correctly" $ do
181+ let rng = mkQCGen 0
182+ burl = BaseUrl Http "localhost" 80 ""
183+ gen = runGenRequest flagsAPI
122184 req = (unGen gen rng 0) burl
123185 qs = C.unpack $ queryString req
124− qs `shouldBe` "one=&two="
186+ qs `shouldBe` "one&two"
187+
188+htmlDocTypesSpec :: Spec
189+htmlDocTypesSpec = describe "HtmlDocTypes" $ do
190+
191+ it "fails HTML without doctype correctly" $ do
192+ err <- withServantServerAndContext docTypeApi ctx noDocTypeServer $ \burl -> do
193+ evalExample $ serverSatisfies docTypeApi burl args
194+ (htmlIncludesDoctype <%> mempty)
195+ show err `shouldContain` "htmlIncludesDoctype"
196+
197+ it "passes HTML with a doctype at start" $ do
198+ withServantServerAndContext docTypeApi ctx docTypeServer $ \burl ->
199+ serverSatisfies docTypeApi burl args (htmlIncludesDoctype <%> mempty)
200+
201+ it "accepts json endpoints and passes over them in silence" $ do
202+ withServantServerAndContext api ctx server $ \burl -> do
203+ serverSatisfies (Proxy :: Proxy (Get '[JSON] Int)) burl args
204+ (htmlIncludesDoctype <%> mempty)
205+
206+
207+makeRandomRequest :: Proxy LargeAPI -> BaseUrl -> IO Integer
208+makeRandomRequest large burl = do
209+ req <- generate $ runGenRequest large
210+ pure $ fst . fromJust . C.readInteger . C.drop 1 . path $ req burl
211+
212+
213+unbiasedGenerationSpec :: Spec
214+unbiasedGenerationSpec = describe "Unbiased Generation of requests" $
215+
216+ it "frequency paired with generated endpoint should be more randomly distributed" $ do
217+ let burl = BaseUrl Http "localhost" 80 ""
218+ let runs = 10000 :: Double
219+ someRequests <- replicateM 10000 (makeRandomRequest largeApi burl)
220+ let mean = (sum $ map fromIntegral someRequests) / runs
221+ let variancer x = let ix = fromIntegral x in (ix - mean) * (ix - mean)
222+ let variance = (sum $ map variancer someRequests) / runs - 1
223+ -- mean should be around 8.5. If this fails, we likely need more runs (or there's a bug!)
224+ mean > 8 `shouldBe` True
225+ mean < 9 `shouldBe` True
226+ -- Std dev is likely around 4. Variance is probably greater than 20.
227+ variance > 19.5 `shouldBe` True
125228
126229------------------------------------------------------------------------------
127230-- APIs
@@ -134,11 +237,17 @@ type API = ReqBody '[JSON] String :> Post '[JSON] String
134237api :: Proxy API
135238api = Proxy
136239
137−type ParamsAPI = QueryParam "one" String :> QueryParam "two" String :> Get '[JSON] ()
240+type ParamsAPI = QueryParam "one" () :> QueryParam "two" () :> Get '[JSON] ()
138241
139242paramsAPI :: Proxy ParamsAPI
140243paramsAPI = Proxy
141244
245+type FlagsAPI = QueryFlag "one" :> QueryFlag "two" :> Get '[JSON] ()
246+
247+flagsAPI :: Proxy FlagsAPI
248+flagsAPI = Proxy
249+
250+
142251server :: IO (Server API)
143252server = do
144253 mvar <- newMVar ""
@@ -152,12 +261,57 @@ type API2 = "failplz" :> Get '[JSON] Int
152261api2 :: Proxy API2
153262api2 = Proxy
154263
264+type DeepAPI = "one" :> "two" :> "three":> Get '[JSON] ()
265+
266+deepAPI :: Proxy DeepAPI
267+deepAPI = Proxy
268+
269+
155270server2 :: IO (Server API2)
156271server2 = return $ return 1
157272
158273server3 :: IO (Server API2)
159274server3 = return $ return 2
160275
276+serverFailing :: IO (Server API2)
277+serverFailing = return . throwError $ err405
278+
279+-- With Doctypes
280+type HtmlDoctype = Get '[HTML] Blaze.Html
281+
282+docTypeApi :: Proxy HtmlDoctype
283+docTypeApi = Proxy
284+
285+docTypeServer :: IO (Server HtmlDoctype)
286+docTypeServer = pure $ pure $ Blaze5.docTypeHtml $ Blaze5.span "Hello Test!"
287+
288+noDocTypeServer :: IO (Server HtmlDoctype)
289+noDocTypeServer = pure $ pure $ Blaze.text "Hello Test!"
290+
291+
292+-- Api for unbiased generation of requests tests
293+largeApi :: Proxy LargeAPI
294+largeApi = Proxy
295+
296+type LargeAPI
297+ = "1" :> Get '[JSON] Int
298+ :<|> "2" :> Get '[JSON] Int
299+ :<|> "3" :> Get '[JSON] Int
300+ :<|> "4" :> Get '[JSON] Int
301+ :<|> "5" :> Get '[JSON] Int
302+ :<|> "6" :> Get '[JSON] Int
303+ :<|> "7" :> Get '[JSON] Int
304+ :<|> "8" :> Get '[JSON] Int
305+ :<|> "9" :> Get '[JSON] Int
306+ :<|> "10" :> Get '[JSON] Int
307+ :<|> "11" :> Get '[JSON] Int
308+ :<|> "12" :> Get '[JSON] Int
309+ :<|> "13" :> Get '[JSON] Int
310+ :<|> "14" :> Get '[JSON] Int
311+ :<|> "15" :> Get '[JSON] Int
312+ :<|> "16" :> Get '[JSON] Int
313+
314+
161315type OctetAPI = Get '[OctetStream] BS.ByteString
162316
163317octetAPI :: Proxy OctetAPI
@@ -166,17 +320,46 @@ octetAPI = Proxy
166320serverOctetAPI :: IO (Server OctetAPI)
167321serverOctetAPI = return $ return "blah"
168322
323+type JsonApi = "jsonComparison" :> Get '[OctetStream] BS.ByteString
324+
325+jsonApi :: Proxy JsonApi
326+jsonApi = Proxy
327+
328+jsonServer1 :: IO (Server JsonApi)
329+jsonServer1 = return $ return "{ \"b\": [\"b\"], \"a\": 1 }" -- whitespace, ordering different
330+
331+jsonServer2 :: IO (Server JsonApi)
332+jsonServer2 = return $ return "{\"a\": 1,\"b\":[\"b\"]}"
333+
334+jsonServer3 :: IO (Server JsonApi)
335+jsonServer3 = return $ return "{\"a\": 2, \"b\": [\"b\"]}"
336+
337+jsonServer4 :: IO (Server JsonApi)
338+jsonServer4 = return $ return "{\"c\": 1, \"d\": [\"b\"]}"
339+
340+
169341ctx :: Context '[BasicAuthCheck ()]
170342ctx = BasicAuthCheck (const . return $ NoSuchUser) :. EmptyContext
171343------------------------------------------------------------------------------
172344-- Utils
173345------------------------------------------------------------------------------
174−
175−evalExample :: (Example e, Arg e ~ ()) => e -> IO Result
176−evalExample e = evaluateExample e defaultParams ($ ()) progCallback
346+evalExample :: (Example e, Arg e ~ ()) => e -> IO EvalResult
347+evalExample e = do
348+ r <- safeEvaluateExample e defaultParams ($ ()) progCallback
349+ case resultStatus r of
350+ Success -> return $ AllGood
351+ Failure _ reason -> return $ FailedWith $ show reason
352+ Pending {} -> error "should not happen"
177353 where
178354 progCallback _ = return ()
179355
356+data EvalResult
357+ = AnException SomeException
358+ | AllGood
359+ | FailedWith String
360+ deriving (Show)
361+
362+
180363args :: Args
181364args = defaultArgs { maxSuccess = noOfTestCases }
182365
@@ -186,8 +369,3 @@ noOfTestCases = 20000
186369#else
187370noOfTestCases = 1000
188371#endif
189−
190−#if !MIN_VERSION_servant(0,8,0)
191−comprehensiveAPIWithoutRaw :: Proxy ComprehensiveAPI
192−comprehensiveAPIWithoutRaw = comprehensiveAPI
193−#endif