git.iko.soy/servant-quickcheck
on master help sign in
17 files changed+1097−309
since splitsnapshots
unifiedsplit
.gitignore+1 .travis.yml+153−21 CHANGELOG.yaml+170−2 cabal.haskell-ci+7 cabal.project+4 example/Main.hs+31 servant-quickcheck.cabal+123−90 src/Servant/QuickCheck.hs+7 src/Servant/QuickCheck/Internal/Equality.hs+35−2 src/Servant/QuickCheck/Internal/ErrorTypes.hs+3−1 src/Servant/QuickCheck/Internal/HasGenRequest.hs+92−47 src/Servant/QuickCheck/Internal/Predicates.hs+136−36 src/Servant/QuickCheck/Internal/QuickCheck.hs+69−27 stack-ghc-8.0.1.yaml−10 stack.yaml+6−3 test/Doctest.hs−44 test/Servant/QuickCheck/InternalSpec.hs+260−26
.gitignore+1
@@ -3,3 +3,4 @@ scripts/
33samples/
44test-servers/
55/doc/
6+.stack-work/
.travis.yml+153−21
@@ -1,26 +1,158 @@
1−sudo: false
2−
1+# This Travis job script has been generated by a script via
2+#
3+# haskell-ci '--config=cabal.haskell-ci' 'servant-quickcheck.cabal'
4+#
5+# To regenerate the script (for example after adjusting tested-with) run
6+#
7+# haskell-ci regenerate
8+#
9+# For more information, see https://github.com/haskell-CI/haskell-ci
10+#
11+# version: 0.10.1
12+#
13+version: ~> 1.0
314language: c
4−
5−matrix:
15+os: linux
16+dist: xenial
17+git:
18+ # whether to recursively clone submodules
19+ submodules: false
20+branches:
21+ only:
22+ - master
23+cache:
24+ directories:
25+ - $HOME/.cabal/packages
26+ - $HOME/.cabal/store
27+ - $HOME/.hlint
28+before_cache:
29+ - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log
30+ # remove files that are regenerated by 'cabal update'
31+ - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*
32+ - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json
33+ - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache
34+ - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar
35+ - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx
36+ - rm -rfv $CABALHOME/packages/head.hackage
37+jobs:
638 include:
7− - env: CABALVER=1.22 GHCVER=7.10.1
8− addons: {apt: {packages: [cabal-install-1.22,ghc-7.10.1],sources: [hvr-ghc]}}
9− - env: CABALVER=1.24 GHCVER=8.0.1
10− addons: {apt: {packages: [cabal-install-1.24,ghc-8.0.1], sources: [hvr-ghc]}}
11−
39+ - compiler: ghc-8.8.3
40+ addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.8.3","cabal-install-3.2"]}}
41+ os: linux
42+ - compiler: ghc-8.6.5
43+ addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.6.5","cabal-install-3.2"]}}
44+ os: linux
45+ - compiler: ghc-8.4.4
46+ addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.4.4","cabal-install-3.2"]}}
47+ os: linux
48+ - compiler: ghc-8.2.2
49+ addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.2.2","cabal-install-3.2"]}}
50+ os: linux
51+ - compiler: ghc-8.0.2
52+ addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.0.2","cabal-install-3.2"]}}
53+ os: linux
54+before_install:
55+ - HC=$(echo "/opt/$CC/bin/ghc" | sed 's/-/\//')
56+ - WITHCOMPILER="-w $HC"
57+ - HADDOCK=$(echo "/opt/$CC/bin/haddock" | sed 's/-/\//')
58+ - HCPKG="$HC-pkg"
59+ - unset CC
60+ - CABAL=/opt/ghc/bin/cabal
61+ - CABALHOME=$HOME/.cabal
62+ - export PATH="$CABALHOME/bin:$PATH"
63+ - TOP=$(pwd)
64+ - "HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')"
65+ - echo $HCNUMVER
66+ - CABAL="$CABAL -vnormal+nowrap"
67+ - set -o pipefail
68+ - TEST=--enable-tests
69+ - BENCH=--enable-benchmarks
70+ - HEADHACKAGE=false
71+ - rm -f $CABALHOME/config
72+ - |
73+ echo "verbose: normal +nowrap +markoutput" >> $CABALHOME/config
74+ echo "remote-build-reporting: anonymous" >> $CABALHOME/config
75+ echo "write-ghc-environment-files: always" >> $CABALHOME/config
76+ echo "remote-repo-cache: $CABALHOME/packages" >> $CABALHOME/config
77+ echo "logs-dir: $CABALHOME/logs" >> $CABALHOME/config
78+ echo "world-file: $CABALHOME/world" >> $CABALHOME/config
79+ echo "extra-prog-path: $CABALHOME/bin" >> $CABALHOME/config
80+ echo "symlink-bindir: $CABALHOME/bin" >> $CABALHOME/config
81+ echo "installdir: $CABALHOME/bin" >> $CABALHOME/config
82+ echo "build-summary: $CABALHOME/logs/build.log" >> $CABALHOME/config
83+ echo "store-dir: $CABALHOME/store" >> $CABALHOME/config
84+ echo "install-dirs user" >> $CABALHOME/config
85+ echo " prefix: $CABALHOME" >> $CABALHOME/config
86+ echo "repository hackage.haskell.org" >> $CABALHOME/config
87+ echo " url: http://hackage.haskell.org/" >> $CABALHOME/config
1288install:
13− - (mkdir -p $HOME/.local/bin && cd $HOME/.local/bin && wget https://zalora-public.s3.amazonaws.com/tinc && chmod +x tinc)
14− - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
15− - ghc --version
16− - cabal --version
17− - travis_retry cabal update
18− - sed -i 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config
19−
89+ - ${CABAL} --version
90+ - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
91+ - |
92+ echo "program-default-options" >> $CABALHOME/config
93+ echo " ghc-options: $GHCJOBS +RTS -M6G -RTS" >> $CABALHOME/config
94+ - cat $CABALHOME/config
95+ - rm -fv cabal.project cabal.project.local cabal.project.freeze
96+ - travis_retry ${CABAL} v2-update -v
97+ # Generate cabal.project
98+ - rm -rf cabal.project cabal.project.local cabal.project.freeze
99+ - touch cabal.project
100+ - |
101+ echo "packages: ." >> cabal.project
102+ - if [ $HCNUMVER -ge 80200 ] ; then echo 'package servant-quickcheck' >> cabal.project ; fi
103+ - "if [ $HCNUMVER -ge 80200 ] ; then echo ' ghc-options: -Werror=missing-methods' >> cabal.project ; fi"
104+ - |
105+ - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(servant-quickcheck)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
106+ - cat cabal.project || true
107+ - cat cabal.project.local || true
108+ - if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi
109+ - ${CABAL} v2-freeze $WITHCOMPILER ${TEST} ${BENCH}
110+ - "cat cabal.project.freeze | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
111+ - rm cabal.project.freeze
112+ - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} --dep -j2 all
113+ - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --dep -j2 all
20114script:
21− - tinc && cabal configure --enable-tests && cabal build && cabal test
22− - cabal check
115+ - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
116+ # Packaging...
117+ - ${CABAL} v2-sdist all
118+ # Unpacking...
119+ - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
120+ - cd ${DISTDIR} || false
121+ - find . -maxdepth 1 -type f -name '*.tar.gz' -exec tar -xvf '{}' \;
122+ - find . -maxdepth 1 -type f -name '*.tar.gz' -exec rm '{}' \;
123+ - PKGDIR_servant_quickcheck="$(find . -maxdepth 1 -type d -regex '.*/servant-quickcheck-[0-9.]*')"
124+ # Generate cabal.project
125+ - rm -rf cabal.project cabal.project.local cabal.project.freeze
126+ - touch cabal.project
127+ - |
128+ echo "packages: ${PKGDIR_servant_quickcheck}" >> cabal.project
129+ - if [ $HCNUMVER -ge 80200 ] ; then echo 'package servant-quickcheck' >> cabal.project ; fi
130+ - "if [ $HCNUMVER -ge 80200 ] ; then echo ' ghc-options: -Werror=missing-methods' >> cabal.project ; fi"
131+ - |
132+ - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(servant-quickcheck)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
133+ - cat cabal.project || true
134+ - cat cabal.project.local || true
135+ # Building...
136+ # this builds all libraries and executables (without tests/benchmarks)
137+ - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all
138+ # Building with tests and benchmarks...
139+ # build & run tests, build benchmarks
140+ - ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} all
141+ # Testing...
142+ - ${CABAL} v2-test $WITHCOMPILER ${TEST} ${BENCH} all
143+ # cabal check...
144+ - (cd ${PKGDIR_servant_quickcheck} && ${CABAL} -vnormal check)
145+ # haddock...
146+ - ${CABAL} v2-haddock $WITHCOMPILER --with-haddock $HADDOCK ${TEST} ${BENCH} all
147+ # Building without installed constraints for packages in global-db...
148+ - rm -f cabal.project.local
149+ - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all
150+ # Constraint sets
151+ - rm -rf cabal.project.local
152+ # Constraint set base-compat-0.10
153+ - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='base-compat ==0.10.*' all
154+ # Constraint set base-compat-0.11
155+ - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='base-compat ==0.11.*' all
23156
24−cache:
25− directories:
26− - $HOME/.tinc/cache
157+# REGENDATA ("0.10.1",["--config=cabal.haskell-ci","servant-quickcheck.cabal"])
158+# EOF
CHANGELOG.yaml+170−2
@@ -1,6 +1,174 @@
1−upcoming:
2−
31releases:
2+ - version: "0.0.9.0"
3+ changes:
4+ - description: "Support servant-0.15 (#65)"
5+ authors: fizruk
6+ date: 2020-06-25
7+
8+ - description: "Relax constraints for GHC 8.10 (#70)"
9+ authors: felixmulder
10+ date: 2020-06-20
11+
12+ - version: "0.0.8.0"
13+ changes:
14+ - description: Support for servant-0.17
15+ authors: phadej
16+ date: 2019-01-23
17+
18+ - version: "0.0.7.3"
19+ changes:
20+ - description: Support for servant-0.14
21+ issue: 53
22+ authors: phadej
23+ date: 2018-06-12
24+
25+ - description: Fix a failure from OnlyJsonObjects when there is no content-type.
26+ issue: 55
27+ authors: Phenitei
28+ date: 2018-08-27
29+
30+ - description: A bug fix where notAllowedContainsAllowHeader would print the initial request alongside the failure instead of the request causing the failure when it failed.
31+ issue: 57
32+ authors: Phenitei
33+ date: 2018-08-29
34+
35+ - description: QuickCheck 2.12 compatibility
36+ issue: 58
37+ authors: parsonsmatt
38+ date: 2018-10-12
39+
40+ - description: GHC 8.6 compatibility
41+ issue: 59
42+ authors: phadej
43+ date: 2018-10-15
44+
45+ - version: "0.0.7.2"
46+ changes:
47+
48+ - description: Allow client to pass an HTTP Manager in to functions
49+ issue: 47
50+ authors: parsonsmatt
51+ date: 2018-05-10
52+
53+ - description: Fix "should not happen" error when exceptions are thrown
54+ issue: 48
55+ authors: parsonsmatt
56+ date: 2018-05-10
57+
58+ - version: "0.0.7.0"
59+ changes:
60+
61+ - description: Support for base-compat-0.10
62+ issue: none
63+ authors: phadej
64+ date: 2018-04-12
65+
66+ - version: "0.0.7.0"
67+ changes:
68+
69+ - description: Support for GHC-8.4.1
70+ issue: none
71+ authors: phadej
72+ date: 2018-03-23
73+
74+ - description: Requires hspec-2.5
75+ issue: none
76+ authors: phadej
77+ date: 2018-03-23
78+
79+ - version: "0.0.6.0"
80+ changes:
81+
82+ - description: Support for servant-0.12
83+ issue: none
84+ authors: phadej
85+ date: 2018-02-09
86+
87+ - version: "0.0.5.0"
88+ changes:
89+
90+ - description: Export forgotten predicates
91+ issue: none
92+ pr: 40
93+ authors: Phenitei
94+ date: 2017-12-14
95+
96+ - version: "0.0.4"
97+ changes:
98+
99+ - description: Support for Servant 0.12
100+ issue: none
101+ authors: phadej
102+ date: 2017-11-07
103+
104+ - description: Support for Servant 0.11
105+ issue: none
106+ pr: 32
107+ authors: adinapoli-iohk
108+ date: 2017-10-18
109+ notes: Includes 0-weighted instance for EmptyAPI
110+
111+ - version: "0.0.3.0"
112+ changes:
113+
114+ - description: Add jsonEquality
115+ issue: 2
116+ pr: 30
117+ authors: erewok
118+ date: 2017-10-15
119+
120+ - description: Support hspec >= 2.4
121+ issue: 27
122+ pr: 29
123+ authors: erewok, jkarni
124+ date: 2017-10-15
125+
126+ - version: "0.0.2.4"
127+ changes:
128+
129+ - description: Don't append slashes to paths
130+ issue: 22
131+ authors: declension
132+ date: 2017-03-11
133+
134+ - version: "0.0.2.3"
135+ changes:
136+
137+ - description: Fix QueryParam and QueryFlag requests
138+ issue: 23
139+ authors: declension
140+ date: 2017-03-10
141+
142+ - version: "0.0.2.2"
143+ changes:
144+
145+ - description: Make onlyJsonObjects succeed in non-JSON endpoints
146+ issue: 20
147+ authors: jkarni
148+ date: 2016-10-18
149+
150+
151+ - version: "0.0.2.1"
152+ changes:
153+
154+ - description: Add notLongerThan predicate
155+ pr: 17
156+ authors: jkarni
157+ date: 2016-10-05
158+
159+ - description: Add getsHaveLastModifiedHeader predicate
160+ pr: none
161+ authors: jkarni
162+ date: 2016-10-03
163+
164+ - description: Raise upper bounds
165+ notes: >
166+ For Quickcheck, aeson, http-client, servant, servant-client and
167+ servant-server.
168+ pr: none
169+ authors: jkarni
170+ date: 2016-10-03
171+
4172
5173 - version: "0.0.2.0"
6174 changes:
cabal.haskell-cinew file+7
@@ -0,0 +1,7 @@
1+branches: master
2+
3+constraint-set base-compat-0.10
4+ constraints: base-compat ==0.10.*
5+
6+constraint-set base-compat-0.11
7+ constraints: base-compat ==0.11.*
cabal.projectnew file+4
@@ -0,0 +1,4 @@
1+packages: .
2+tests: true
3+
4+allow-newer: servant-blaze:servant
example/Main.hsnew file+31
@@ -0,0 +1,31 @@
1+{-# LANGUAGE OverloadedStrings, DataKinds #-}
2+module Main (main) where
3+
4+import Servant
5+import Servant.QuickCheck
6+import Test.Hspec
7+import Data.Text (Text)
8+import System.Environment (getArgs)
9+
10+main :: IO ()
11+main = do
12+ args <- getArgs
13+ case args of
14+ [] -> putStrLn "Not running without arguments. Try --help or --fail-fast."
15+ _ -> hspec spec
16+
17+-- Change to String to reproduce
18+-- https://github.com/haskell-servant/servant-quickcheck/issues/41
19+type API = Get '[PlainText] Text
20+
21+api :: Proxy API
22+api = Proxy
23+
24+server :: Server API
25+server = return "Sigurð Fáfnirslayer"
26+
27+spec :: Spec
28+spec = describe "example server" $
29+ it "mangles UTF-8 in error messages" $
30+ withServantServer api (return server) $ \burl ->
31+ serverSatisfies api burl defaultArgs (getsHaveCacheControlHeader <%> mempty)
servant-quickcheck.cabal+123−90
@@ -1,22 +1,22 @@
1−name: servant-quickcheck
2−version: 0.0.2.0
3−synopsis: QuickCheck entire APIs
1+name: servant-quickcheck
2+version: 0.0.9.1
3+synopsis: QuickCheck entire APIs
44description:
5− This packages provides QuickCheck properties that are tested across an entire
6− API.
5+ This packages provides QuickCheck properties that are tested across an entire
6+ API.
77
8−license: BSD3
9−license-file: LICENSE
10−author: Julian K. Arni
11−maintainer: jkarni@gmail.com
12−category: Web
13−build-type: Simple
14−cabal-version: >=1.10
15−extra-source-files:
16− CHANGELOG.yaml
8+license: BSD3
9+license-file: LICENSE
10+author: Julian K. Arni
11+maintainer: jkarni@gmail.com
12+category: Web
13+build-type: Simple
14+cabal-version: >=1.10
15+extra-source-files: CHANGELOG.yaml
16+tested-with: GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || == 8.8.3
1717
1818source-repository head
19− type: git
19+ type: git
2020 location: https://github.com/haskell-servant/servant-quickcheck
2121
2222flag long-tests
@@ -24,82 +24,115 @@ flag long-tests
2424 default: False
2525
2626library
27− exposed-modules: Servant.QuickCheck
28− , Servant.QuickCheck.Internal
29− , Servant.QuickCheck.Internal.Predicates
30− , Servant.QuickCheck.Internal.HasGenRequest
31− , Servant.QuickCheck.Internal.QuickCheck
32− , Servant.QuickCheck.Internal.Equality
33− , Servant.QuickCheck.Internal.ErrorTypes
34− build-depends: base >=4.8 && <4.10
35− , base-compat == 0.9.*
36− , aeson > 0.8 && < 0.12
37− , bytestring == 0.10.*
38− , case-insensitive == 1.2.*
39− , data-default-class >= 0.0 && < 0.2
40− , hspec == 2.2.*
41− , http-client >= 0.4.30 && < 0.5
42− , http-media == 0.6.*
43− , http-types > 0.8 && < 0.10
44− , mtl > 2.1 && < 2.3
45− , pretty == 1.1.*
46− , process >= 1.2 && < 1.5
47− , QuickCheck > 2.7 && < 2.9
48− , servant > 0.6 && < 0.9
49− , servant-client > 0.6 && < 0.9
50− , servant-server > 0.6 && < 0.9
51− , split == 0.2.*
52− , string-conversions > 0.3 && < 0.5
53− , temporary == 1.2.*
54− , text == 1.*
55− , warp >= 3.2.4 && < 3.3
27+ exposed-modules:
28+ Servant.QuickCheck
29+ Servant.QuickCheck.Internal
30+ Servant.QuickCheck.Internal.Equality
31+ Servant.QuickCheck.Internal.ErrorTypes
32+ Servant.QuickCheck.Internal.HasGenRequest
33+ Servant.QuickCheck.Internal.Predicates
34+ Servant.QuickCheck.Internal.QuickCheck
5635
57− hs-source-dirs: src
58− default-extensions: TypeOperators
59− , FlexibleInstances
60− , FlexibleContexts
61− , DataKinds
62− , GADTs
63− , MultiParamTypeClasses
64− , DeriveFunctor
65− , KindSignatures
66− , RankNTypes
67− , ConstraintKinds
68− , DeriveGeneric
69− , ScopedTypeVariables
70− , OverloadedStrings
71− , FunctionalDependencies
72− , NoImplicitPrelude
73− , DeriveDataTypeable
74− default-language: Haskell2010
36+ build-depends:
37+ aeson >=0.8 && <2
38+ , base >=4.9 && <4.15
39+ , base-compat-batteries >=0.10.1 && <0.12
40+ , bytestring >=0.10 && <0.11
41+ , case-insensitive >=1.2 && <1.3
42+ , clock >=0.7 && <0.9
43+ , data-default-class >=0.0 && <0.2
44+ , hspec >=2.5.6 && <2.8
45+ , http-client >=0.4.30 && <0.8
46+ , http-media >=0.6 && <0.9
47+ , http-types >=0.8 && <0.13
48+ , mtl >=2.1 && <2.3
49+ , pretty >=1.1 && <1.2
50+ , process >=1.2 && <1.7
51+ , QuickCheck >=2.7 && <2.15
52+ , servant >=0.17 && <0.19
53+ , servant-client >=0.17 && <0.19
54+ , servant-server >=0.17 && <0.19
55+ , split >=0.2 && <0.3
56+ , string-conversions >=0.3 && <0.5
57+ , temporary >=1.2 && <1.4
58+ , text >=1 && <2
59+ , time >=1.5 && <1.11
60+ , warp >=3.2.4 && <3.4
61+
62+ if !impl(ghc >=8.0)
63+ build-depends: semigroups >=0.18.3 && <0.20
64+
65+ hs-source-dirs: src
66+ default-extensions:
67+ NoImplicitPrelude
68+ ConstraintKinds
69+ DataKinds
70+ DeriveDataTypeable
71+ DeriveFunctor
72+ DeriveGeneric
73+ FlexibleContexts
74+ FlexibleInstances
75+ FunctionalDependencies
76+ GADTs
77+ KindSignatures
78+ MultiParamTypeClasses
79+ OverloadedStrings
80+ RankNTypes
81+ ScopedTypeVariables
82+ TypeOperators
83+
84+ default-language: Haskell2010
7585
7686test-suite spec
77− type: exitcode-stdio-1.0
78− ghc-options: -Wall -threaded
79− default-language: Haskell2010
80− hs-source-dirs: test
81− main-is: Spec.hs
82− other-modules: Servant.QuickCheck.InternalSpec
83− build-depends: base == 4.*
84− , base-compat
85− , servant-quickcheck
86− , hspec
87− , hspec-core
88− , http-client
89− , warp
90− , servant-server
91− , servant-client
92− , servant
93− , transformers
94− , QuickCheck
95− , quickcheck-io
96− default-extensions: TypeOperators
97− , FlexibleInstances
98− , FlexibleContexts
99− , GADTs
100− , DataKinds
101− , NoImplicitPrelude
102− , OverloadedStrings
103− , ScopedTypeVariables
87+ type: exitcode-stdio-1.0
88+ ghc-options: -Wall -threaded
89+ default-language: Haskell2010
90+ hs-source-dirs: test
91+ main-is: Spec.hs
92+ other-modules: Servant.QuickCheck.InternalSpec
93+ build-tool-depends: hspec-discover:hspec-discover -any
94+ build-depends:
95+ aeson
96+ , base
97+ , base-compat-batteries
98+ , blaze-html
99+ , bytestring
100+ , hspec
101+ , hspec-core >=2.5.5 && <2.8
102+ , http-client
103+ , QuickCheck
104+ , quickcheck-io
105+ , servant
106+ , servant-blaze
107+ , servant-client
108+ , servant-quickcheck
109+ , servant-server
110+ , transformers
111+ , warp
112+
113+ default-extensions:
114+ NoImplicitPrelude
115+ DataKinds
116+ FlexibleContexts
117+ FlexibleInstances
118+ GADTs
119+ OverloadedStrings
120+ ScopedTypeVariables
121+ TypeOperators
122+
104123 if flag(long-tests)
105− cpp-options: -DLONG_TESTS
124+ cpp-options: -DLONG_TESTS
125+
126+test-suite example
127+ type: exitcode-stdio-1.0
128+ main-is: Main.hs
129+ hs-source-dirs: example
130+ ghc-options: -Wall
131+ build-depends:
132+ base
133+ , hspec
134+ , servant-quickcheck
135+ , servant-server
136+ , text
137+
138+ default-language: Haskell2010
src/Servant/QuickCheck.hs+7
@@ -27,12 +27,18 @@ module Servant.QuickCheck
2727 -- in RFCs. The __Best Practices__ includes, in addition to RFC
2828 -- recommendations, recommendations found elsewhere or generally accepted.
2929 , not500
30+ , notLongerThan
3031 , onlyJsonObjects
32+ , honoursAcceptHeader
3133 , notAllowedContainsAllowHeader
3234 , unauthorizedContainsWWWAuthenticate
35+ , getsHaveLastModifiedHeader
3336 , getsHaveCacheControlHeader
3437 , headsHaveCacheControlHeader
3538 , createContainsValidLocation
39+ -- * Html Predicates
40+ , htmlIncludesDoctype
41+
3642 -- *** Predicate utilities and types
3743 , (<%>)
3844 , Predicates
@@ -48,6 +54,7 @@ module Servant.QuickCheck
4854 -- represents other means of checking equality
4955 -- *** Useful @ResponseEquality@s
5056 , bodyEquality
57+ , jsonEquality
5158 , allEquality
5259 -- ** Response equality type
5360 , ResponseEquality(..)
src/Servant/QuickCheck/Internal/Equality.hs+35−2
@@ -1,16 +1,23 @@
11module Servant.QuickCheck.Internal.Equality where
22
3+import Data.Aeson (Value, decode, decodeStrict)
4+import Data.ByteString (ByteString)
5+import qualified Data.ByteString.Lazy as LB
36import Data.Function (on)
47import Network.HTTP.Client (Response, responseBody)
8+import Data.Semigroup (Semigroup (..))
59import Prelude.Compat
610
711newtype ResponseEquality b
812 = ResponseEquality { getResponseEquality :: Response b -> Response b -> Bool }
913
14+instance Semigroup (ResponseEquality b) where
15+ ResponseEquality a <> ResponseEquality b = ResponseEquality $ \x y ->
16+ a x y && b x y
17+
1018instance Monoid (ResponseEquality b) where
1119 mempty = ResponseEquality $ \_ _ -> True
12− ResponseEquality a `mappend` ResponseEquality b = ResponseEquality $ \x y ->
13− a x y && b x y
20+ mappend = (<>)
1421
1522-- | Use `Eq` instance for `Response`
1623--
@@ -23,3 +30,29 @@ allEquality = ResponseEquality (==)
2330-- /Since 0.0.0.0/
2431bodyEquality :: Eq b => ResponseEquality b
2532bodyEquality = ResponseEquality ((==) `on` responseBody)
33+
34+-- | Equality as 'Value'. This means that if two bodies are equal as JSON
35+-- (e.g., insignificant whitespace difference) they are considered equal.
36+--
37+-- /Since 0.0.3.0/
38+jsonEquality :: (JsonEq b) => ResponseEquality b
39+jsonEquality = ResponseEquality (jsonEq `on` responseBody)
40+
41+class JsonEq a where
42+ decode' :: a -> Maybe Value
43+ jsonEq :: a -> a -> Bool
44+ jsonEq first second = compareDecodedResponses (decode' first) (decode' second)
45+
46+instance JsonEq LB.ByteString where
47+ decode' = decode
48+
49+instance JsonEq ByteString where
50+ decode' = decodeStrict
51+
52+compareDecodedResponses :: Maybe Value -> Maybe Value -> Bool
53+compareDecodedResponses resp1 resp2 =
54+ case resp1 of
55+ Nothing -> False -- if decoding fails we assume failure
56+ (Just r1) -> case resp2 of
57+ Nothing -> False -- another decode failure
58+ (Just r2) -> r1 == r2
src/Servant/QuickCheck/Internal/ErrorTypes.hs+3−1
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12module Servant.QuickCheck.Internal.ErrorTypes where
23
34import Control.Exception (Exception (..))
@@ -8,9 +9,10 @@ import Data.Typeable (Typeable)
89import GHC.Generics (Generic)
910import qualified Network.HTTP.Client as C
1011import Network.HTTP.Types (Header, statusCode)
11−import Prelude.Compat
1212import Text.PrettyPrint
1313
14+import Prelude.Compat hiding ((<>))
15+
1416data PredicateFailure
1517 = PredicateFailure T.Text (Maybe C.Request) (C.Response LBS.ByteString)
1618 deriving (Typeable, Generic)
src/Servant/QuickCheck/Internal/HasGenRequest.hs+92−47
@@ -2,112 +2,147 @@
22{-# LANGUAGE PolyKinds #-}
33module Servant.QuickCheck.Internal.HasGenRequest where
44
5−import Data.Default.Class (def)
6−import Data.Monoid ((<>))
75import Data.String (fromString)
86import Data.String.Conversions (cs)
9−import qualified Data.ByteString as BS
107import GHC.TypeLits (KnownSymbol, Nat, symbolVal)
118import Network.HTTP.Client (Request, RequestBody (..), host, method, path,
129 port, queryString, requestBody, requestHeaders,
13− secure)
10+ secure, defaultRequest)
1411import Network.HTTP.Media (renderHeader)
1512import Prelude.Compat
1613import Servant
1714import Servant.API.ContentTypes (AllMimeRender (..))
1815import Servant.Client (BaseUrl (..), Scheme (..))
19−import Test.QuickCheck (Arbitrary (..), Gen, elements, oneof)
16+import Test.QuickCheck (Arbitrary (..), Gen, elements, frequency)
17+
18+import qualified Data.ByteString as BS
19+import qualified Data.ByteString.Internal as BS (c2w)
20+
21+
22+-- -----------------------------------------------------------------------------
23+-- runGenRequest
24+
25+-- | This function returns a QuickCheck `Gen a` when passed a servant API value,
26+-- typically a `Proxy API`. The generator returned is a function
27+-- that accepts a `BaseUrl` and returns a `Request`, which can then be used
28+-- to issue network requests. This `Gen` type makes it easier to compare distinct
29+-- APIs across different `BaseUrl`s.
30+runGenRequest :: HasGenRequest a => Proxy a -> Gen (BaseUrl -> Request)
31+runGenRequest = snd . genRequest
32+
2033
34+-- -----------------------------------------------------------------------------
35+-- HasGenRequest
2136
37+-- | This is the core Servant-Quickcheck generator, which, when given a `Proxy API`
38+-- will return a pair of `Int` and `Gen a`, where `a` is a function from
39+-- `BaseUrl` to a `Network.Http.Client.Request`. The `Int` is a weight for the
40+-- QuickCheck `frequency` function which ensures a random distribution across
41+-- all endpoints in an API.
2242class HasGenRequest a where
23− genRequest :: Proxy a -> Gen (BaseUrl -> Request)
43+ genRequest :: Proxy a -> (Int, Gen (BaseUrl -> Request))
44+
2445
2546instance (HasGenRequest a, HasGenRequest b) => HasGenRequest (a :<|> b) where
2647 genRequest _
27− = oneof [ genRequest (Proxy :: Proxy a)
28− , genRequest (Proxy :: Proxy b)
29− ]
48+ = (lf + rf, frequency [l, r])
49+ where
50+ l@(lf, _) = genRequest (Proxy :: Proxy a)
51+ r@(rf, _) = genRequest (Proxy :: Proxy b)
52+
3053
3154instance (KnownSymbol path, HasGenRequest b) => HasGenRequest (path :> b) where
32− genRequest _ = do
55+ genRequest _ = (oldf, do
3356 old' <- old
34− return $ \burl -> let r = old' burl in r { path = new <> path r }
57+ return $ \burl -> let r = old' burl
58+ oldPath = path r
59+ oldPath' = BS.dropWhile (== BS.c2w '/') oldPath
60+ paths = filter (not . BS.null) [new, oldPath']
61+ in r { path = "/" <> BS.intercalate "/" paths })
3562 where
36− old = genRequest (Proxy :: Proxy b)
63+ (oldf, old) = genRequest (Proxy :: Proxy b)
3764 new = cs $ symbolVal (Proxy :: Proxy path)
3865
66+instance HasGenRequest EmptyAPI where
67+ genRequest _ = (0, error "EmptyAPIs cannot be queried.")
68+
69+instance HasGenRequest api => HasGenRequest (Summary d :> api) where
70+ genRequest _ = genRequest (Proxy :: Proxy api)
71+
72+instance HasGenRequest api => HasGenRequest (Description d :> api) where
73+ genRequest _ = genRequest (Proxy :: Proxy api)
74+
3975instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
40− => HasGenRequest (Capture x c :> b) where
41− genRequest _ = do
76+ => HasGenRequest (Capture' mods x c :> b) where
77+ genRequest _ = (oldf, do
4278 old' <- old
4379 new' <- toUrlPiece <$> new
44− return $ \burl -> let r = old' burl in r { path = cs new' <> path r }
80+ return $ \burl -> let r = old' burl in r { path = cs new' <> path r })
4581 where
46− old = genRequest (Proxy :: Proxy b)
82+ (oldf, old) = genRequest (Proxy :: Proxy b)
4783 new = arbitrary :: Gen c
4884
49−#if MIN_VERSION_servant(0,8,0)
5085instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
5186 => HasGenRequest (CaptureAll x c :> b) where
52− genRequest _ = do
87+ genRequest _ = (oldf, do
5388 old' <- old
5489 new' <- fmap (cs . toUrlPiece) <$> new
5590 let new'' = BS.intercalate "/" new'
56− return $ \burl -> let r = old' burl in r { path = new'' <> path r }
91+ return $ \burl -> let r = old' burl in r { path = new'' <> path r })
5792 where
58− old = genRequest (Proxy :: Proxy b)
93+ (oldf, old) = genRequest (Proxy :: Proxy b)
5994 new = arbitrary :: Gen [c]
60−#endif
6195
6296instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c)
63− => HasGenRequest (Header h c :> b) where
64− genRequest _ = do
97+ => HasGenRequest (Header' mods h c :> b) where
98+ genRequest _ = (oldf, do
6599 old' <- old
66− new' <- toUrlPiece <$> new
100+ new' <- toUrlPiece <$> new -- TODO: generate lenient or/and optional
67101 return $ \burl -> let r = old' burl in r {
68− requestHeaders = (hdr, cs new') : requestHeaders r }
102+ requestHeaders = (hdr, cs new') : requestHeaders r })
69103 where
70− old = genRequest (Proxy :: Proxy b)
104+ (oldf, old) = genRequest (Proxy :: Proxy b)
71105 hdr = fromString $ symbolVal (Proxy :: Proxy h)
72106 new = arbitrary :: Gen c
73107
74108instance (AllMimeRender x c, Arbitrary c, HasGenRequest b)
75− => HasGenRequest (ReqBody x c :> b) where
76− genRequest _ = do
77− old' <- old
109+ => HasGenRequest (ReqBody' mods x c :> b) where
110+ genRequest _ = (oldf, do
111+ old' <- old -- TODO: generate lenient
78112 new' <- new
79113 (ct, bd) <- elements $ allMimeRender (Proxy :: Proxy x) new'
80114 return $ \burl -> let r = old' burl in r {
81115 requestBody = RequestBodyLBS bd
82116 , requestHeaders = ("Content-Type", renderHeader ct) : requestHeaders r
83− }
117+ })
84118 where
85− old = genRequest (Proxy :: Proxy b)
119+ (oldf, old) = genRequest (Proxy :: Proxy b)
86120 new = arbitrary :: Gen c
87121
88122instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
89− => HasGenRequest (QueryParam x c :> b) where
90− genRequest _ = do
91− new' <- new
123+ => HasGenRequest (QueryParam' mods x c :> b) where
124+ genRequest _ = (oldf, do
125+ new' <- new -- TODO: generate lenient or/and optional
92126 old' <- old
93− return $ \burl -> let r = old' burl in r {
94− queryString = queryString r
95− <> param <> "=" <> cs (toQueryParam new') }
127+ return $ \burl -> let r = old' burl
128+ newExpr = param <> "=" <> cs (toQueryParam new')
129+ qs = queryString r in r {
130+ queryString = if BS.null qs then newExpr else newExpr <> "&" <> qs })
96131 where
97− old = genRequest (Proxy :: Proxy b)
132+ (oldf, old) = genRequest (Proxy :: Proxy b)
98133 param = cs $ symbolVal (Proxy :: Proxy x)
99134 new = arbitrary :: Gen c
100135
101136instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
102137 => HasGenRequest (QueryParams x c :> b) where
103− genRequest _ = do
138+ genRequest _ = (oldf, do
104139 new' <- new
105140 old' <- old
106141 return $ \burl -> let r = old' burl in r {
107142 queryString = queryString r
108− <> if length new' > 0 then fold (toParam <$> new') else ""}
143+ <> if length new' > 0 then fold (toParam <$> new') else ""})
109144 where
110− old = genRequest (Proxy :: Proxy b)
145+ (oldf, old) = genRequest (Proxy :: Proxy b)
111146 param = cs $ symbolVal (Proxy :: Proxy x)
112147 new = arbitrary :: Gen [c]
113148 toParam c = param <> "[]=" <> cs (toQueryParam c)
@@ -115,22 +150,32 @@ instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
115150
116151instance (KnownSymbol x, HasGenRequest b)
117152 => HasGenRequest (QueryFlag x :> b) where
118− genRequest _ = do
153+ genRequest _ = (oldf, do
119154 old' <- old
120− return $ \burl -> let r = old' burl in r {
121− queryString = queryString r <> param <> "=" }
155+ return $ \burl -> let r = old' burl
156+ qs = queryString r in r {
157+ queryString = if BS.null qs then param else param <> "&" <> qs })
122158 where
123− old = genRequest (Proxy :: Proxy b)
159+ (oldf, old) = genRequest (Proxy :: Proxy b)
124160 param = cs $ symbolVal (Proxy :: Proxy x)
125161
126162instance (ReflectMethod method)
127163 => HasGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) where
128− genRequest _ = return $ \burl -> def
164+ genRequest _ = (1, return $ \burl -> defaultRequest
165+ { host = cs $ baseUrlHost burl
166+ , port = baseUrlPort burl
167+ , secure = baseUrlScheme burl == Https
168+ , method = reflectMethod (Proxy :: Proxy method)
169+ })
170+
171+instance (ReflectMethod method)
172+ => HasGenRequest (NoContentVerb (method :: k)) where
173+ genRequest _ = (1, return $ \burl -> defaultRequest
129174 { host = cs $ baseUrlHost burl
130175 , port = baseUrlPort burl
131176 , secure = baseUrlScheme burl == Https
132177 , method = reflectMethod (Proxy :: Proxy method)
133− }
178+ })
134179
135180instance (HasGenRequest a) => HasGenRequest (RemoteHost :> a) where
136181 genRequest _ = genRequest (Proxy :: Proxy a)
src/Servant/QuickCheck/Internal/Predicates.hs+136−36
@@ -1,26 +1,33 @@
11module Servant.QuickCheck.Internal.Predicates where
22
33import Control.Exception (catch, throw)
4−import Control.Monad (when, unless, liftM2)
4+import Control.Monad (liftM2, unless, when)
55import Data.Aeson (Object, decode)
6+import Data.Bifunctor (first)
67import qualified Data.ByteString as SBS
78import qualified Data.ByteString.Char8 as SBSC
89import qualified Data.ByteString.Lazy as LBS
9−import Data.CaseInsensitive (mk)
10+import Data.CaseInsensitive (foldCase, foldedCase, mk)
1011import Data.Either (isRight)
1112import Data.List.Split (wordsBy)
1213import Data.Maybe (fromMaybe, isJust)
13−import Data.Monoid ((<>))
14+import Data.Semigroup (Semigroup (..))
15+import qualified Data.Text as T
16+import Data.Time (UTCTime, defaultTimeLocale, parseTimeM,
17+ rfc822DateFormat)
1418import GHC.Generics (Generic)
1519import Network.HTTP.Client (Manager, Request, Response, httpLbs,
16− method, requestHeaders, responseBody,
17− responseHeaders, parseRequest, responseStatus)
20+ method, parseRequest, requestHeaders,
21+ responseBody, responseHeaders,
22+ responseStatus)
1823import Network.HTTP.Media (matchAccept)
1924import Network.HTTP.Types (methodGet, methodHead, parseMethod,
2025 renderStdMethod, status100, status200,
2126 status201, status300, status401,
2227 status405, status500)
2328import Prelude.Compat
29+import System.Clock (Clock (Monotonic), diffTimeSpec,
30+ getTime, toNanoSecs)
2431
2532import Servant.QuickCheck.Internal.ErrorTypes
2633
@@ -36,7 +43,23 @@ import Servant.QuickCheck.Internal.ErrorTypes
3643-- /Since 0.0.0.0/
3744not500 :: ResponsePredicate
3845not500 = ResponsePredicate $ \resp ->
39− when (responseStatus resp == status500) $ fail "not500"
46+ when (responseStatus resp == status500) $ throw $ PredicateFailure "not500" Nothing resp
47+
48+-- | [__Optional__]
49+--
50+-- This function checks that the response from the server does not take longer
51+-- than the specified number of nanoseconds.
52+--
53+-- /Since 0.0.2.1/
54+notLongerThan :: Integer -> RequestPredicate
55+notLongerThan maxAllowed
56+ = RequestPredicate $ \req mgr -> do
57+ start <- getTime Monotonic
58+ resp <- httpLbs req mgr
59+ end <- getTime Monotonic
60+ when (toNanoSecs (end `diffTimeSpec` start) > maxAllowed) $
61+ throw $ PredicateFailure "notLongerThan" (Just req) resp
62+ return []
4063
4164-- | [__Best Practice__]
4265--
@@ -61,9 +84,13 @@ not500 = ResponsePredicate $ \resp ->
6184-- /Since 0.0.0.0/
6285onlyJsonObjects :: ResponsePredicate
6386onlyJsonObjects
64− = ResponsePredicate (\resp -> case decode (responseBody resp) of
65− Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp
66− Just (_ :: Object) -> return ())
87+ = ResponsePredicate (\resp -> do
88+ case lookup "content-type" (first foldedCase <$> responseHeaders resp) of
89+ Nothing -> return ()
90+ Just ctype -> when ("application/json" `SBS.isPrefixOf` ctype) $ do
91+ case (decode (responseBody resp) :: Maybe Object) of
92+ Nothing -> throw $ PredicateFailure "onlyJsonObjects" Nothing resp
93+ Just _ -> return ())
6794
6895-- | __Optional__
6996--
@@ -91,21 +118,52 @@ createContainsValidLocation
91118 resp <- httpLbs req mgr
92119 if responseStatus resp == status201
93120 then case lookup "Location" $ responseHeaders resp of
94− Nothing -> fail n
121+ Nothing -> throw $ PredicateFailure n (Just req) resp
95122 Just l -> case parseRequest $ SBSC.unpack l of
96− Nothing -> fail n
123+ Nothing -> throw $ PredicateFailure n (Just req) resp
97124 Just x -> do
98125 resp2 <- httpLbs x mgr
99− status2XX resp2 n
126+ status2XX (Just req) resp2 n
100127 return [resp, resp2]
101128 else return [resp]
102129
103−{-
104−getsHaveLastModifiedHeader :: ResponsePredicate
130+-- | [__Optional__]
131+--
132+-- The @Last-Modified@ header represents the time a resource was last
133+-- modified. It is used to drive caching and conditional requests.
134+--
135+-- When using this mechanism, the server adds the @Last-Modified@ header to
136+-- responses. Clients may then make requests with the @If-Modified-Since@
137+-- header to conditionally request resources. If the resource has not
138+-- changed since that date, the server responds with a status code of 304
139+-- (@Not Modified@) without a response body.
140+--
141+-- The @Last-Modified@ header can also be used in conjunction with the
142+-- @If-Unmodified-Since@ header to drive optimistic concurrency.
143+--
144+-- The @Last-Modified@ date must be in RFC 822 format.
145+--
146+-- __References__:
147+--
148+-- * 304 Not Modified: <https://tools.ietf.org/html/rfc7232#section-4.1 RFC 7232 Section 4.1>
149+-- * Last-Modified header: <https://tools.ietf.org/html/rfc7232#section-2.2 RFC 7232 Section 2.2>
150+-- * If-Modified-Since header: <https://tools.ietf.org/html/rfc7232#section-3.3 RFC 7232 Section 3.3>
151+-- * If-Unmodified-Since header: <https://tools.ietf.org/html/rfc7232#section-3.4 RFC 7232 Section 3.4>
152+-- * Date format: <https://tools.ietf.org/html/rfc2616#section-3.3 RFC 2616 Section 3.3>
153+--
154+-- /Since 0.0.2.1/
155+getsHaveLastModifiedHeader :: RequestPredicate
105156getsHaveLastModifiedHeader
106− = ResponsePredicate "getsHaveLastModifiedHeader" (\resp ->
157+ = RequestPredicate $ \req mgr ->
158+ if (method req == methodGet)
159+ then do
160+ resp <- httpLbs req mgr
161+ unless (hasValidHeader "Last-Modified" isRFC822Date resp) $ do
162+ throw $ PredicateFailure "getsHaveLastModifiedHeader" (Just req) resp
163+ return [resp]
164+ else return []
165+
107166
108−-}
109167
110168-- | [__RFC Compliance__]
111169--
@@ -117,23 +175,27 @@ getsHaveLastModifiedHeader
117175-- This function checks that every @405 Method Not Allowed@ response contains
118176-- an @Allow@ header with a list of standard HTTP methods.
119177--
178+-- Note that 'servant' itself does not currently set the @Allow@ headers.
179+--
120180-- __References__:
121181--
122182-- * @Allow@ header: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html RFC 2616 Section 14.7>
123183-- * Status 405: <https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html RFC 2616 Section 10.4.6>
184+-- * Servant Allow header issue: <https://github.com/haskell-servant/servant/issues/489 Issue #489>
124185--
125186-- /Since 0.0.0.0/
126187notAllowedContainsAllowHeader :: RequestPredicate
127188notAllowedContainsAllowHeader
128189 = RequestPredicate $ \req mgr -> do
129− resp <- mapM (flip httpLbs mgr) $ [ req { method = renderStdMethod m }
130− | m <- [minBound .. maxBound ]
131− , renderStdMethod m /= method req ]
132− case filter pred' resp of
133− (x:_) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just req) x
190+ let reqs = [ req { method = renderStdMethod m } | m <- [minBound .. maxBound]
191+ , renderStdMethod m /= method req ]
192+ resp <- mapM (flip httpLbs mgr) reqs
193+
194+ case filter pred' (zip reqs resp) of
195+ (x:_) -> throw $ PredicateFailure "notAllowedContainsAllowHeader" (Just $ fst x) (snd x)
134196 [] -> return resp
135197 where
136− pred' resp = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
198+ pred' (_, resp) = responseStatus resp == status405 && not (hasValidHeader "Allow" go resp)
137199 where
138200 go x = all (\y -> isRight $ parseMethod $ SBSC.pack y)
139201 $ wordsBy (`elem` (", " :: [Char])) (SBSC.unpack x)
@@ -163,7 +225,7 @@ honoursAcceptHeader
163225 sacc = fromMaybe "*/*" $ lookup "Accept" (requestHeaders req)
164226 if status100 < scode && scode < status300
165227 then if isJust $ sctype >>= \x -> matchAccept [x] sacc
166− then fail "honoursAcceptHeader"
228+ then throw $ PredicateFailure "honoursAcceptHeader" (Just req) resp
167229 else return [resp]
168230 else return [resp]
169231
@@ -274,7 +336,29 @@ unauthorizedContainsWWWAuthenticate
274336 = ResponsePredicate $ \resp ->
275337 if responseStatus resp == status401
276338 then unless (hasValidHeader "WWW-Authenticate" (const True) resp) $
277− fail "unauthorizedContainsWWWAuthenticate"
339+ throw $ PredicateFailure "unauthorizedContainsWWWAuthenticate" Nothing resp
340+ else return ()
341+
342+
343+-- | [__RFC Compliance__]
344+--
345+-- [An HTML] document will start with exactly this string: <!DOCTYPE html>
346+--
347+-- This function checks that HTML documents (those with `Content-Type: text/html...`)
348+-- include a DOCTYPE declaration at the top. We do not enforce capital case for the string `DOCTYPE`.
349+--
350+-- __References__:
351+--
352+-- * HTML5 Doctype: <https://tools.ietf.org/html/rfc7992#section-6.1 RFC 7992 Section 6.1>
353+-- /Since 0.3.0.0/
354+htmlIncludesDoctype :: ResponsePredicate
355+htmlIncludesDoctype
356+ = ResponsePredicate $ \resp ->
357+ if hasValidHeader "Content-Type" (SBS.isPrefixOf . foldCase $ "text/html") resp
358+ then do
359+ let htmlContent = foldCase . LBS.take 20 $ responseBody resp
360+ unless (LBS.isPrefixOf (foldCase "<!doctype html>") htmlContent) $
361+ throw $ PredicateFailure "htmlIncludesDoctype" Nothing resp
278362 else return ()
279363
280364-- * Predicate logic
@@ -288,37 +372,47 @@ unauthorizedContainsWWWAuthenticate
288372-- | A predicate that depends only on the response.
289373--
290374-- /Since 0.0.0.0/
291−data ResponsePredicate = ResponsePredicate
375+newtype ResponsePredicate = ResponsePredicate
292376 { getResponsePredicate :: Response LBS.ByteString -> IO ()
293377 } deriving (Generic)
294378
379+instance Semigroup ResponsePredicate where
380+ ResponsePredicate a <> ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
381+
295382instance Monoid ResponsePredicate where
296383 mempty = ResponsePredicate $ const $ return ()
297− ResponsePredicate a `mappend` ResponsePredicate b = ResponsePredicate $ \x -> a x >> b x
384+ mappend = (<>)
298385
299386-- | A predicate that depends on both the request and the response.
300387--
301388-- /Since 0.0.0.0/
302−data RequestPredicate = RequestPredicate
303− { getRequestPredicate :: Request -> Manager -> IO [Response LBS.ByteString]
389+newtype RequestPredicate = RequestPredicate
390+ { getRequestPredicate :: Request -> Manager -> IO [Response LBS.ByteString]
304391 } deriving (Generic)
305392
306393-- TODO: This isn't actually a monoid
307394instance Monoid RequestPredicate where
308395 mempty = RequestPredicate (\r m -> httpLbs r m >>= \x -> return ([x]))
309− RequestPredicate a `mappend` RequestPredicate b = RequestPredicate $ \r mgr ->
396+ mappend = (<>)
397+
398+-- TODO: This isn't actually a monoid
399+instance Semigroup RequestPredicate where
400+ RequestPredicate a <> RequestPredicate b = RequestPredicate $ \r mgr ->
310401 liftM2 (<>) (a r mgr) (b r mgr)
311402
312403-- | A set of predicates. Construct one with 'mempty' and '<%>'.
313404data Predicates = Predicates
314− { requestPredicates :: RequestPredicate
405+ { requestPredicates :: RequestPredicate
315406 , responsePredicates :: ResponsePredicate
316407 } deriving (Generic)
317408
409+instance Semigroup Predicates where
410+ a <> b = Predicates (requestPredicates a <> requestPredicates b)
411+ (responsePredicates a <> responsePredicates b)
412+
318413instance Monoid Predicates where
319414 mempty = Predicates mempty mempty
320− a `mappend` b = Predicates (requestPredicates a <> requestPredicates b)
321− (responsePredicates a <> responsePredicates b)
415+ mappend = (<>)
322416
323417class JoinPreds a where
324418 joinPreds :: a -> Predicates -> Predicates
@@ -354,8 +448,14 @@ hasValidHeader hdr p r = case lookup (mk hdr) (responseHeaders r) of
354448 Nothing -> False
355449 Just v -> p v
356450
357−status2XX :: Monad m => Response b -> String -> m ()
358−status2XX r t
359− | status200 <= responseStatus r && responseStatus r < status300
451+isRFC822Date :: SBS.ByteString -> Bool
452+isRFC822Date s
453+ = case parseTimeM True defaultTimeLocale rfc822DateFormat (SBSC.unpack s) of
454+ Nothing -> False
455+ Just (_ :: UTCTime) -> True
456+
457+status2XX :: Monad m => Maybe Request -> Response LBS.ByteString -> T.Text -> m ()
458+status2XX mreq resp t
459+ | status200 <= responseStatus resp && responseStatus resp < status300
360460 = return ()
361− | otherwise = fail t
461+ | otherwise = throw $ PredicateFailure t mreq resp
src/Servant/QuickCheck/Internal/QuickCheck.hs+69−27
@@ -1,7 +1,8 @@
11{-# LANGUAGE RecordWildCards #-}
2+{-# LANGUAGE CPP #-}
23module Servant.QuickCheck.Internal.QuickCheck where
34
4−import Control.Concurrent (modifyMVar_, newMVar, readMVar)
5+import Control.Concurrent (tryReadMVar, newEmptyMVar, tryPutMVar)
56import Control.Monad (unless)
67import qualified Data.ByteString.Lazy as LBS
78import Data.Proxy (Proxy)
@@ -10,11 +11,13 @@ import Network.Wai.Handler.Warp (withApplication)
1011import Prelude.Compat
1112import Servant (Context (EmptyContext), HasServer,
1213 Server, serveWithContext)
14+#if MIN_VERSION_servant_server(0,18,0)
15+import Servant (DefaultErrorFormatters, ErrorFormatters, HasContextEntry, type (.++))
16+#endif
1317import Servant.Client (BaseUrl (..), Scheme (..))
1418import System.IO.Unsafe (unsafePerformIO)
1519import Test.Hspec (Expectation, expectationFailure)
16−import Test.QuickCheck (Args (..), Result (..),
17− quickCheckWithResult)
20+import Test.QuickCheck (Args (..), Result (..), quickCheckWithResult)
1821import Test.QuickCheck.Monadic (assert, forAllM, monadicIO, monitor,
1922 run)
2023import Test.QuickCheck.Property (counterexample)
@@ -37,7 +40,11 @@ withServantServer api = withServantServerAndContext api EmptyContext
3740-- application.
3841--
3942-- /Since 0.0.0.0/
43+#if MIN_VERSION_servant_server(0,18,0)
44+withServantServerAndContext :: (HasServer a ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters)
45+#else
4046withServantServerAndContext :: HasServer a ctx
47+#endif
4148 => Proxy a -> Context ctx -> IO (Server a) -> (BaseUrl -> IO r) -> IO r
4249withServantServerAndContext api ctx server t
4350 = withApplication (return . serveWithContext api ctx =<< server) $ \port ->
@@ -70,25 +77,32 @@ withServantServerAndContext api ctx server t
7077serversEqual :: HasGenRequest a =>
7178 Proxy a -> BaseUrl -> BaseUrl -> Args -> ResponseEquality LBS.ByteString -> Expectation
7279serversEqual api burl1 burl2 args req = do
73− let reqs = (\f -> (f burl1, f burl2)) <$> genRequest api
80+ let reqs = (\f -> (f burl1, f burl2)) <$> runGenRequest api
7481 -- This MVar stuff is clunky! But there doesn't seem to be an easy way to
7582 -- return results when a test fails, since an exception is throw.
76− deetsMVar <- newMVar $ error "should not be called"
83+ deetsMVar <- newEmptyMVar
7784 r <- quickCheckWithResult args { chatty = False } $ monadicIO $ forAllM reqs $ \(req1, req2) -> do
7885 resp1 <- run $ C.httpLbs (noCheckStatus req1) defManager
7986 resp2 <- run $ C.httpLbs (noCheckStatus req2) defManager
8087 unless (getResponseEquality req resp1 resp2) $ do
8188 monitor (counterexample "hi" )
82− run $ modifyMVar_ deetsMVar $ const $ return $
83− ServerEqualityFailure req1 resp1 resp2
89+ _ <- run $ tryPutMVar deetsMVar $ ServerEqualityFailure req1 resp1 resp2
8490 assert False
8591 case r of
8692 Success {} -> return ()
87− Failure{..} -> readMVar deetsMVar >>= \x -> expectationFailure $
88− "Failed:\n" ++ show x
93+ Failure{..} -> do
94+ mx <- tryReadMVar deetsMVar
95+ case mx of
96+ Just x ->
97+ expectationFailure $ "Failed:\n" ++ show x
98+ Nothing ->
99+ expectationFailure $ "We failed to record a reason for failure: " <> show r
89100 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
90− NoExpectedFailure {} -> expectationFailure $ "No expected failure"
91− InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
101+ NoExpectedFailure {} -> expectationFailure "No expected failure"
102+#if MIN_VERSION_QuickCheck(2,12,0)
103+#else
104+ InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
105+#endif
92106
93107-- | Check that a server satisfies the set of properties specified.
94108--
@@ -111,40 +125,68 @@ serversEqual api burl1 burl2 args req = do
111125-- /Since 0.0.0.0/
112126serverSatisfies :: (HasGenRequest a) =>
113127 Proxy a -> BaseUrl -> Args -> Predicates -> Expectation
114−serverSatisfies api burl args preds = do
115− let reqs = ($ burl) <$> genRequest api
116− deetsMVar <- newMVar $ error "should not be called"
128+serverSatisfies api = serverSatisfiesMgr api defManager
129+
130+-- | Check that a server satisfies the set of properties specified, and
131+-- accept a 'Manager' for running the HTTP requests through.
132+--
133+-- See 'serverSatisfies' for more details.
134+--
135+-- @since 0.0.7.2
136+serverSatisfiesMgr :: (HasGenRequest a) =>
137+ Proxy a -> C.Manager -> BaseUrl -> Args -> Predicates -> Expectation
138+serverSatisfiesMgr api manager burl args preds = do
139+ let reqs = ($ burl) <$> runGenRequest api
140+ deetsMVar <- newEmptyMVar
117141 r <- quickCheckWithResult args { chatty = False } $ monadicIO $ forAllM reqs $ \req -> do
118− v <- run $ finishPredicates preds (noCheckStatus req) defManager
119− run $ modifyMVar_ deetsMVar $ const $ return v
120− case v of
121− Just _ -> assert False
122− _ -> return ()
142+ v <- run $ finishPredicates preds (noCheckStatus req) manager
143+ _ <- run $ tryPutMVar deetsMVar v
144+ case v of
145+ Just _ -> assert False
146+ _ -> return ()
123147 case r of
124148 Success {} -> return ()
125− Failure{..} -> readMVar deetsMVar >>= \x -> expectationFailure $
126− "Failed:\n" ++ show x
149+ Failure {..} -> do
150+ mx <- tryReadMVar deetsMVar
151+ case mx of
152+ Just x ->
153+ expectationFailure $ "Failed:\n" ++ show x
154+ Nothing ->
155+ expectationFailure $ "We failed to record a reason for failure: " <> show r
127156 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
128157 NoExpectedFailure {} -> expectationFailure $ "No expected failure"
129− InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
130−
158+#if MIN_VERSION_QuickCheck(2,12,0)
159+#else
160+ InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
161+#endif
131162
132163serverDoesntSatisfy :: (HasGenRequest a) =>
133164 Proxy a -> BaseUrl -> Args -> Predicates -> Expectation
134−serverDoesntSatisfy api burl args preds = do
135− let reqs = ($ burl) <$> genRequest api
165+serverDoesntSatisfy api = serverDoesntSatisfyMgr api defManager
166+
167+serverDoesntSatisfyMgr :: (HasGenRequest a) =>
168+ Proxy a -> C.Manager -> BaseUrl -> Args -> Predicates -> Expectation
169+serverDoesntSatisfyMgr api manager burl args preds = do
170+ let reqs = ($ burl) <$> runGenRequest api
136171 r <- quickCheckWithResult args $ monadicIO $ forAllM reqs $ \req -> do
137− v <- run $ finishPredicates preds (noCheckStatus req) defManager
172+ v <- run $ finishPredicates preds (noCheckStatus req) manager
138173 assert $ not $ null v
139174 case r of
140175 Success {} -> return ()
141176 GaveUp { numTests = n } -> expectationFailure $ "Gave up after " ++ show n ++ " tests"
142177 Failure { output = m } -> expectationFailure $ "Failed:\n" ++ show m
143178 NoExpectedFailure {} -> expectationFailure $ "No expected failure"
144− InsufficientCoverage {} -> expectationFailure $ "Insufficient coverage"
179+#if MIN_VERSION_QuickCheck(2,12,0)
180+#else
181+ InsufficientCoverage {} -> expectationFailure "Insufficient coverage"
182+#endif
145183
146184noCheckStatus :: C.Request -> C.Request
185+#if MIN_VERSION_http_client(0,5,0)
186+noCheckStatus = id
187+#else
147188noCheckStatus r = r { C.checkStatus = \_ _ _ -> Nothing}
189+#endif
148190
149191defManager :: C.Manager
150192defManager = unsafePerformIO $ C.newManager C.defaultManagerSettings
stack-ghc-8.0.1.yamldeleted−10
@@ -1,10 +0,0 @@
1−# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
2−resolver: nightly-2016-09-07
3−packages:
4−- '.'
5−extra-deps:
6−- 'servant-0.8.1'
7−- 'servant-server-0.8.1'
8−- 'servant-client-0.8.1'
9−flags: {}
10−extra-package-dbs: []
stack.yaml+6−3
@@ -1,7 +1,10 @@
1−# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
2−resolver: lts-6.17
1+resolver: nightly-2018-09-03
32packages:
43- '.'
5−extra-deps: []
4+extra-deps:
5+- hspec-discover-2.5.6
6+- hspec-core-2.5.6
7+- hspec-2.5.6
8+- QuickCheck-2.12
69flags: {}
710extra-package-dbs: []
test/Doctest.hsdeleted−44
@@ -1,44 +0,0 @@
1−module Main where
2−
3−import Data.List (isPrefixOf)
4−import System.Directory
5−import System.FilePath
6−import System.FilePath.Find
7−import Test.DocTest
8−
9−main :: IO ()
10−main = do
11− files <- find always (extension ==? ".hs") "src"
12− mCabalMacrosFile <- getCabalMacrosFile
13− doctest $ "-isrc" : "-Iinclude" :
14− (maybe [] (\ f -> ["-optP-include", "-optP" ++ f]) mCabalMacrosFile) ++
15− "-XOverloadedStrings" :
16− "-XDeriveFunctor" :
17− "-XFlexibleInstances" :
18− "-XFlexibleContexts" :
19− "-XMultiParamTypeClasses" :
20− "-XDataKinds" :
21− "-XTypeOperators" :
22− "-XGADTs" :
23− files
24−
25−getCabalMacrosFile :: IO (Maybe FilePath)
26−getCabalMacrosFile = do
27− exists <- doesDirectoryExist "dist"
28− if exists
29− then do
30− contents <- getDirectoryContents "dist"
31− let rest = "build" </> "autogen" </> "cabal_macros.h"
32− whenExists $ case filter ("dist-sandbox-" `isPrefixOf`) contents of
33− [x] -> "dist" </> x </> rest
34− [] -> "dist" </> rest
35− xs -> error $ "ran doctests with multiple dist/dist-sandbox-xxxxx's: \n"
36− ++ show xs ++ "\nTry cabal clean"
37− else return Nothing
38− where
39− whenExists :: FilePath -> IO (Maybe FilePath)
40− whenExists file = do
41− exists <- doesFileExist file
42− return $ if exists
43− then Just file
44− else Nothing
test/Servant/QuickCheck/InternalSpec.hs+260−26
@@ -1,19 +1,33 @@
11{-# LANGUAGE CPP #-}
22module Servant.QuickCheck.InternalSpec (spec) where
33
4−import Control.Concurrent.MVar (newMVar, readMVar, swapMVar)
5−import Control.Monad.IO.Class (liftIO)
6−import Prelude.Compat
7−import Servant
8−import Servant.API.Internal.Test.ComprehensiveAPI (comprehensiveAPIWithoutRaw)
9−import Test.Hspec (Spec, context, describe, it,
10− shouldBe, shouldContain)
11−import Test.Hspec.Core.Spec (Arg, Example, Result (..),
12− defaultParams,
13− evaluateExample)
4+
5+import Control.Concurrent.MVar (newMVar, readMVar, swapMVar)
6+import Control.Exception (SomeException)
7+import Control.Monad (replicateM)
8+import Control.Monad.IO.Class (liftIO)
9+import qualified Data.ByteString as BS
10+import qualified Data.ByteString.Char8 as C
11+import Data.Maybe (fromJust)
12+import Network.HTTP.Client (path, queryString)
13+import Prelude.Compat
14+import Servant
15+import Servant.HTML.Blaze (HTML)
16+import qualified Text.Blaze.Html as Blaze
17+import qualified Text.Blaze.Html5 as Blaze5
18+import Test.Hspec (Spec, context, describe, it, shouldBe,
19+ shouldContain)
20+import Test.Hspec.Core.Spec (Arg, Example, Result (..), ResultStatus (..),
21+ defaultParams, safeEvaluateExample)
22+import Test.QuickCheck.Gen (generate, unGen)
23+import Test.QuickCheck.Random (mkQCGen)
24+
25+
26+import Servant.Test.ComprehensiveAPI (comprehensiveAPIWithoutStreamingOrRaw)
1427
1528import Servant.QuickCheck
16−import Servant.QuickCheck.Internal (genRequest, serverDoesntSatisfy)
29+import Servant.QuickCheck.Internal (genRequest, runGenRequest,
30+ serverDoesntSatisfy)
1731
1832spec :: Spec
1933spec = do
@@ -21,6 +35,12 @@ spec = do
2135 serverSatisfiesSpec
2236 isComprehensiveSpec
2337 onlyJsonObjectSpec
38+ notLongerThanSpec
39+ queryParamsSpec
40+ queryFlagsSpec
41+ deepPathSpec
42+ htmlDocTypesSpec
43+ unbiasedGenerationSpec
2444
2545serversEqualSpec :: Spec
2646serversEqualSpec = describe "serversEqual" $ do
@@ -31,15 +51,42 @@ serversEqualSpec = describe "serversEqual" $ do
3151 serversEqual api burl1 burl2 args bodyEquality
3252
3353 context "when servers are not equal" $ do
34−
35−
3654 it "provides the failing responses in the error message" $ do
37− Fail _ err <- withServantServer api2 server2 $ \burl1 ->
55+ FailedWith err <- withServantServer api2 server2 $ \burl1 ->
3856 withServantServer api2 server3 $ \burl2 -> do
3957 evalExample $ serversEqual api2 burl1 burl2 args bodyEquality
58+ show err `shouldContain` "Server equality failed"
4059 show err `shouldContain` "Body: 1"
4160 show err `shouldContain` "Body: 2"
42− 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+
4390
4491serverSatisfiesSpec :: Spec
4592serverSatisfiesSpec = describe "serverSatisfies" $ do
@@ -61,29 +108,123 @@ serverSatisfiesSpec = describe "serverSatisfies" $ do
61108 context "when predicates are false" $ do
62109
63110 it "fails with informative error messages" $ do
64− Fail _ err <- withServantServerAndContext api ctx server $ \burl -> do
65− evalExample $ serverSatisfies api burl args (getsHaveCacheControlHeader <%> mempty)
66− err `shouldContain` "getsHaveCacheControlHeader"
67− err `shouldContain` "Headers"
68− 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+
69117
70118onlyJsonObjectSpec :: Spec
71119onlyJsonObjectSpec = describe "onlyJsonObjects" $ do
72120
73121 it "fails correctly" $ do
74− Fail _ err <- withServantServerAndContext api ctx server $ \burl -> do
122+ FailedWith err <- withServantServerAndContext api ctx server $ \burl -> do
75123 evalExample $ serverSatisfies (Proxy :: Proxy (Get '[JSON] Int)) burl args
76124 (onlyJsonObjects <%> mempty)
77− err `shouldContain` "onlyJsonObjects"
125+ show err `shouldContain` "onlyJsonObjects"
126+
127+ it "accepts non-JSON endpoints" $ do
128+ withServantServerAndContext octetAPI ctx serverOctetAPI $ \burl ->
129+ serverSatisfies octetAPI burl args (onlyJsonObjects <%> mempty)
130+
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)
78134
135+notLongerThanSpec :: Spec
136+notLongerThanSpec = describe "notLongerThan" $ do
137+
138+ it "fails correctly" $ do
139+ FailedWith err <- withServantServerAndContext api ctx server $ \burl -> do
140+ evalExample $ serverSatisfies (Proxy :: Proxy (Get '[JSON] Int)) burl args
141+ (notLongerThan 1 <%> mempty)
142+ show err `shouldContain` "notLongerThan"
143+
144+ it "succeeds correctly" $ do
145+ withServantServerAndContext api ctx server $ \burl ->
146+ serverSatisfies api burl args (notLongerThan 1000000000000 <%> mempty)
79147
80148isComprehensiveSpec :: Spec
81149isComprehensiveSpec = describe "HasGenRequest" $ do
82150
83151 it "has instances for all 'servant' combinators" $ do
84− let _g = genRequest comprehensiveAPIWithoutRaw
152+ let _g = genRequest comprehensiveAPIWithoutStreamingOrRaw
85153 True `shouldBe` True -- This is a type-level check
86154
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+
166+queryParamsSpec :: Spec
167+queryParamsSpec = describe "QueryParams" $ do
168+
169+ it "reduce to an HTTP query string correctly" $ do
170+ let rng = mkQCGen 0
171+ burl = BaseUrl Http "localhost" 80 ""
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
184+ req = (unGen gen rng 0) burl
185+ qs = C.unpack $ queryString req
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
87228
88229------------------------------------------------------------------------------
89230-- APIs
@@ -96,6 +237,17 @@ type API = ReqBody '[JSON] String :> Post '[JSON] String
96237api :: Proxy API
97238api = Proxy
98239
240+type ParamsAPI = QueryParam "one" () :> QueryParam "two" () :> Get '[JSON] ()
241+
242+paramsAPI :: Proxy ParamsAPI
243+paramsAPI = Proxy
244+
245+type FlagsAPI = QueryFlag "one" :> QueryFlag "two" :> Get '[JSON] ()
246+
247+flagsAPI :: Proxy FlagsAPI
248+flagsAPI = Proxy
249+
250+
99251server :: IO (Server API)
100252server = do
101253 mvar <- newMVar ""
@@ -109,23 +261,105 @@ type API2 = "failplz" :> Get '[JSON] Int
109261api2 :: Proxy API2
110262api2 = Proxy
111263
264+type DeepAPI = "one" :> "two" :> "three":> Get '[JSON] ()
265+
266+deepAPI :: Proxy DeepAPI
267+deepAPI = Proxy
268+
269+
112270server2 :: IO (Server API2)
113271server2 = return $ return 1
114272
115273server3 :: IO (Server API2)
116274server3 = return $ return 2
117275
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+
315+type OctetAPI = Get '[OctetStream] BS.ByteString
316+
317+octetAPI :: Proxy OctetAPI
318+octetAPI = Proxy
319+
320+serverOctetAPI :: IO (Server OctetAPI)
321+serverOctetAPI = return $ return "blah"
322+
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+
118341ctx :: Context '[BasicAuthCheck ()]
119342ctx = BasicAuthCheck (const . return $ NoSuchUser) :. EmptyContext
120343------------------------------------------------------------------------------
121344-- Utils
122345------------------------------------------------------------------------------
123−
124−evalExample :: (Example e, Arg e ~ ()) => e -> IO Result
125−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"
126353 where
127354 progCallback _ = return ()
128355
356+data EvalResult
357+ = AnException SomeException
358+ | AllGood
359+ | FailedWith String
360+ deriving (Show)
361+
362+
129363args :: Args
130364args = defaultArgs { maxSuccess = noOfTestCases }
131365