From 1d44db32f0f0289ebe9828e38ef6689b15143549 Mon Sep 17 00:00:00 2001 From: Alaudidae Lark Date: Fri, 5 May 2017 00:16:45 +0530 Subject: [PATCH] initial version with basic model - todo implement logic added a swagger-generator deriving coupontype automatically implemented cors for swagger to work fixed product model --- .directory | 4 +++ .gitignore | 26 ++++++++++++++ .vscode/settings.json | 3 ++ LICENSE | 30 ++++++++++++++++ README.md | 1 + SPEC.md | 23 +++++++++++++ Setup.hs | 2 ++ app/Main.hs | 11 ++++++ coupon-servant.cabal | 70 +++++++++++++++++++++++++++++++++++++ src/Api.hs | 24 +++++++++++++ src/App.hs | 80 +++++++++++++++++++++++++++++++++++++++++++ src/Coupon.hs | 50 +++++++++++++++++++++++++++ src/Lib.hs | 40 ++++++++++++++++++++++ src/Models.hs | 47 +++++++++++++++++++++++++ src/SwaggerGen.hs | 38 ++++++++++++++++++++ stack.yaml | 66 +++++++++++++++++++++++++++++++++++ swagger.json | 1 + test/Spec.hs | 20 +++++++++++ 18 files changed, 536 insertions(+) create mode 100644 .directory create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SPEC.md create mode 100644 Setup.hs create mode 100644 app/Main.hs create mode 100644 coupon-servant.cabal create mode 100644 src/Api.hs create mode 100644 src/App.hs create mode 100644 src/Coupon.hs create mode 100644 src/Lib.hs create mode 100644 src/Models.hs create mode 100644 src/SwaggerGen.hs create mode 100644 stack.yaml create mode 100644 swagger.json create mode 100644 test/Spec.hs diff --git a/.directory b/.directory new file mode 100644 index 0000000..f19d2f8 --- /dev/null +++ b/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2017,5,3,7,47,12 +Version=4 +ViewMode=1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0621534 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ + +# Created by https://www.gitignore.io/api/haskell + +### Haskell ### +dist +dist-* +cabal-dev +*.o +*.hi +*.chi +*.chs.h +*.dyn_o +*.dyn_hi +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +*.prof +*.aux +*.hp +*.eventlog +.stack-work/ +cabal.project.local +.HTF/ + +# End of https://www.gitignore.io/api/haskell diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..20af2f6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +// Place your settings in this file to overwrite default and user settings. +{ +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a042c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2017 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..918e56a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# coupon-servant diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..9106057 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,23 @@ +Imagine the backend API of a typical coupon module that would be getting used in shopping cart applications. +End-customers would typically be discovering these coupons via some promotion or deal sites (like CouponRaja or CouponDunia). +Entering this coupon during the checkout process would give the customer some discount. + +* Design & implement a REST API to create and read coupons. + +* We should be able to define the applicable discount based on +-- Flat discount amount per order +-- Flat discount amount per item +-- Percentage discount on total order amount + +* We should be able to restrict coupon usage based on the following +-- All products or list of products +-- Orders placed between given start/end dates +-- Orders with total amount higher than given value +-- Limit coupon usage per customer (tracked by customer email), i.e. customer is allowed to use a coupon on 'N' times +-- Limit coupon usage per product, i.e. coupon is allowed to be used 'N' number of times for a given product(s) +-- Limit coupon usage, i.e. coupon is allowed to be used 'N' number of times across all customers and products + +* Design & implement a REST API to validate coupon usage and respond with the final discount amount. +The API can accept a list of items/products added to the shopping card, +and the coupon that the customer is trying to apply. +The API should respond with whether the coupon is applicable or not, and discount amount. \ No newline at end of file diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..60e9402 --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,11 @@ +module Main where + +import App + +import SwaggerGen + +main :: IO () +main = do genSwaggerDoc + run "host=localhost port=5432 user=msfuser dbname=coupon password=" + +-- run "testSql.db" diff --git a/coupon-servant.cabal b/coupon-servant.cabal new file mode 100644 index 0000000..3549ee9 --- /dev/null +++ b/coupon-servant.cabal @@ -0,0 +1,70 @@ +name: coupon-servant +version: 0.1.0.0 +-- synopsis: +-- description: +homepage: https://github.com/alaudiadae/coupon-servant#readme +license: BSD3 +license-file: LICENSE +author: Author name here +maintainer: alaudiadae@gmx.com +copyright: 2017 Author name here +category: Web +build-type: Simple +extra-source-files: README.md +cabal-version: >=1.10 + +library + hs-source-dirs: src + exposed-modules: Lib, + Api, + App, + Coupon, + Models, + SwaggerGen + build-depends: base >= 4.7 && < 5 + , aeson + , bytestring + , lens + , monad-logger + , persistent + , persistent-template + , persistent-sqlite + , persistent-postgresql + , servant + , servant-server + , servant-swagger + , string-conversions + , swagger2 + , text + , time + , transformers + , unordered-containers + , wai + , wai-cors + , warp + default-language: Haskell2010 + +executable coupon-servant-exe + hs-source-dirs: app + main-is: Main.hs + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: base + , coupon-servant + default-language: Haskell2010 + +test-suite coupon-servant-test + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs + build-depends: base + , coupon-servant + , hspec + , hspec-wai + , hspec-wai-json + , aeson + ghc-options: -threaded -rtsopts -with-rtsopts=-N + default-language: Haskell2010 + +source-repository head + type: git + location: https://github.com/githubuser/coupon-servant diff --git a/src/Api.hs b/src/Api.hs new file mode 100644 index 0000000..2ba140a --- /dev/null +++ b/src/Api.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Api (module Api,module Models) where + +import Data.Proxy +import Data.Text +import Models +import Servant.API + +type CouponApi = + "coupon" :> "add" :> ReqBody '[JSON] Coupon :> Post '[JSON] (Maybe Coupon) + :<|> "coupon" :> "get" :> Capture "name" Text :> Get '[JSON] (Maybe Coupon) + :<|> "coupon" :> "del" :> Capture "name" Text :> Get '[JSON] (Maybe Coupon) + +type BillCouponApi = + "billcoupon" :> ReqBody '[JSON] BillCoupon :> Post '[JSON] CouponResult + +type ServerApi = CouponApi :<|> BillCouponApi + +api :: Proxy ServerApi +api = Proxy diff --git a/src/App.hs b/src/App.hs new file mode 100644 index 0000000..b97b90d --- /dev/null +++ b/src/App.hs @@ -0,0 +1,80 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module App where + +import Api +import Control.Monad.IO.Class +import Control.Monad.Logger (runStderrLoggingT) +import Data.String.Conversions +import Data.Text +import Database.Persist +import Database.Persist.Postgresql +import Database.Persist.Sqlite +import Network.Wai +import Network.Wai.Handler.Warp as Warp +import Network.Wai.Middleware.Cors +import Servant +-- import Network.Wai.Middleware.RequestLogger (logStdoutDev) + +couponServer :: ConnectionPool -> Server CouponApi +couponServer pool = + couponAddH :<|> couponGetH :<|> couponDelH + where + couponAddH newCoupon = liftIO $ couponAdd newCoupon + couponGetH code = liftIO $ couponGet code + couponDelH code = liftIO $ couponDel code + + couponAdd :: Coupon -> IO (Maybe Coupon) + couponAdd newCoupon = flip runSqlPersistMPool pool $ do + exists <- selectFirst [CouponCode ==. couponCode newCoupon] [] + case exists of + Nothing -> Just <$> insert newCoupon + Just _ -> return Nothing + return Nothing + + couponGet :: Text -> IO (Maybe Coupon) + couponGet code = flip runSqlPersistMPool pool $ do + mUser <- selectFirst [CouponCode ==. code] [] + return $ entityVal <$> mUser + + couponDel :: Text -> IO (Maybe Coupon) + couponDel code = flip runSqlPersistMPool pool $ do + deleteWhere [CouponCode ==. code] + return Nothing + +billCouponServer :: ConnectionPool -> Server BillCouponApi +billCouponServer pool = billCouponComputeH + where billCouponComputeH bill = liftIO $ billCouponCompute bill + -- return $ Applied 100 + billCouponCompute :: BillCoupon -> IO CouponResult + billCouponCompute bill = do putStrLn $ show bill + return $ Applied 100 + + +server :: ConnectionPool -> Server ServerApi +server pool = couponServer pool :<|> billCouponServer pool + + +app :: ConnectionPool -> Application +app pool = cors (const $ Just policy) $ serve api $ server pool + where + policy = simpleCorsResourcePolicy { corsRequestHeaders = ["Content-Type"] } + +mkPgApp :: String -> IO Application +mkPgApp sqliteFile = do + pool <- runStderrLoggingT $ createPostgresqlPool (cs sqliteFile) 5 + runSqlPool (runMigration migrateAll) pool + return $ app pool + +mkApp :: String -> IO Application +mkApp sqliteFile = do + pool <- runStderrLoggingT $ createSqlitePool (cs sqliteFile) 5 + runSqlPool (runMigration migrateAll) pool + return $ app pool + +run :: String -> IO () +run dbConnStr = + Warp.run 3000 =<< mkPgApp dbConnStr diff --git a/src/Coupon.hs b/src/Coupon.hs new file mode 100644 index 0000000..a13c7fd --- /dev/null +++ b/src/Coupon.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Coupon where + +import Data.Aeson +import Data.Aeson.Types +import Database.Persist.TH +import GHC.Generics +import Prelude + + +data Product = Product { + productName :: String, + productPrice:: Int +} deriving (Eq, Read, Show, Generic, FromJSON, ToJSON) + +data BillCoupon = BillCoupon { + customer :: String, + coupon :: String, + productList :: [Product] +} deriving (Eq, Read, Show, Generic, FromJSON, ToJSON) + +data CouponResult = Applied Int | Rejected String | Partial String + deriving (Show, Read, Eq, Generic, FromJSON, ToJSON) + +data CouponType = ProductFlat Int | CartFlat Int | CartPercent Int + deriving (Show, Read, Eq, Generic) + +couponOption = defaultOptions { sumEncoding = ObjectWithSingleField } + +instance FromJSON CouponType where + parseJSON = genericParseJSON couponOption + +instance ToJSON CouponType where + toJSON = genericToJSON couponOption + +derivePersistField "CouponType" + +prodListEx = [Product {productName = "Water", productPrice = 15}] +billCouponExample = BillCoupon { customer = "test@email.com", coupon = "FLAT100", productList = prodListEx} diff --git a/src/Lib.hs b/src/Lib.hs new file mode 100644 index 0000000..3844204 --- /dev/null +++ b/src/Lib.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeOperators #-} +module Lib + ( startApp + , app + ) where + +import Data.Aeson +import Data.Aeson.TH +import Network.Wai +import Network.Wai.Handler.Warp +import Servant + +data User = User + { userId :: Int + , userFirstName :: String + , userLastName :: String + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''User) + +type API = "users" :> Get '[JSON] [User] + +startApp :: IO () +startApp = run 8080 app + +app :: Application +app = serve api server + +api :: Proxy API +api = Proxy + +server :: Server API +server = return users + +users :: [User] +users = [ User 1 "Isaac" "Newton" + , User 2 "Albert" "Einstein" + ] diff --git a/src/Models.hs b/src/Models.hs new file mode 100644 index 0000000..d479c6b --- /dev/null +++ b/src/Models.hs @@ -0,0 +1,47 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Models (module Models,module Coupon) where + +import Coupon +import Data.Aeson +import Data.Text +import Data.Time.Clock +import Database.Persist.TH +import GHC.Generics + +share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| +Customer + email Text + UniqueEmail email + Primary email + deriving Eq Read Show Generic +-- Product +-- name Text +-- price Int +-- code Text +-- UniqueName name +-- Foreign Coupon fkcoupon code +-- Primary name +-- deriving Eq Read Show Generic +Coupon json + code Text + value CouponType + min_price Int + customer_limit Int + usage_limit Int + valid_from UTCTime default=now() + valid_till UTCTime default=now() + UniqueCode code + Primary code + deriving Eq Read Show Generic +|] diff --git a/src/SwaggerGen.hs b/src/SwaggerGen.hs new file mode 100644 index 0000000..3d98327 --- /dev/null +++ b/src/SwaggerGen.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeOperators #-} + +module SwaggerGen where + +import Api +import Control.Lens +import Data.Aeson +import Data.Aeson.Types (camelTo2) +import qualified Data.ByteString.Lazy.Char8 as BL8 +import Data.Swagger +import Servant.Swagger + +modifier :: String -> String +modifier = drop 1 . dropWhile (/= '_') . camelTo2 '_' + +prefixSchemaOptions :: SchemaOptions +prefixSchemaOptions = defaultSchemaOptions { fieldLabelModifier = modifier } + +instance ToSchema BillCoupon where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions +instance ToSchema CouponType where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions +instance ToSchema Coupon where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions +instance ToSchema Product where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions +instance ToSchema Customer where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions +instance ToSchema CouponResult where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions + +swaggerDoc :: Swagger +swaggerDoc = toSwagger api + & host ?~ "localhost:3000" + & info.title .~ "Coupon Api" + & info.version .~ "v1" + +genSwaggerDoc :: IO () +genSwaggerDoc = BL8.writeFile "swagger.json" (encode swaggerDoc) diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..39d6999 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,66 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# http://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# resolver: ghcjs-0.1.0_ghc-7.10.2 +# resolver: +# name: custom-snapshot +# location: "./custom-snapshot.yaml" +resolver: lts-8.13 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# extra-dep: true +# subdirs: +# - auto-update +# - wai +# +# A package marked 'extra-dep: true' will only be built if demanded by a +# non-dependency (i.e. a user package), and its test suites and benchmarks +# will not be run. This is useful for tweaking upstream packages. +packages: +- '.' +# Dependency packages to be pulled from upstream that are not in the resolver +# (e.g., acme-missiles-0.3) +extra-deps: [] + +# Override default flag values for local packages and extra-deps +flags: {} + +# Extra package databases containing global packages +extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.4" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor \ No newline at end of file diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..49aa261 --- /dev/null +++ b/swagger.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"version":"v1","title":"Coupon Api"},"host":"localhost:3000","paths":{"/coupon/add":{"post":{"consumes":["application/json"],"produces":["application/json"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/Coupon"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/Coupon"},"description":""}}}},"/coupon/get/{name}":{"get":{"produces":["application/json"],"parameters":[{"required":true,"in":"path","name":"name","type":"string"}],"responses":{"404":{"description":"`name` not found"},"200":{"schema":{"$ref":"#/definitions/Coupon"},"description":""}}}},"/coupon/del/{name}":{"get":{"produces":["application/json"],"parameters":[{"required":true,"in":"path","name":"name","type":"string"}],"responses":{"404":{"description":"`name` not found"},"200":{"schema":{"$ref":"#/definitions/Coupon"},"description":""}}}},"/billcoupon":{"post":{"consumes":["application/json"],"produces":["application/json"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/BillCoupon"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/CouponResult"},"description":""}}}}},"definitions":{"Coupon":{"required":["code","value","min_price","valid_from","valid_till"],"properties":{"code":{"type":"string"},"value":{"$ref":"#/definitions/CouponType"},"min_price":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"valid_from":{"$ref":"#/definitions/UTCTime"},"valid_till":{"$ref":"#/definitions/UTCTime"}},"type":"object"},"CouponType":{"properties":{"ProductFlat":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"CartFlat":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"CartPercent":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"maxProperties":1,"minProperties":1,"type":"object"},"UTCTime":{"example":"2016-07-22T00:00:00Z","format":"yyyy-mm-ddThh:MM:ssZ","type":"string"},"CouponResult":{"properties":{"Applied":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"Rejected":{"type":"string"},"Partial":{"type":"string"}},"maxProperties":1,"minProperties":1,"type":"object"},"BillCoupon":{"required":["list"],"properties":{"list":{"items":{"type":"string"},"type":"array"}},"minItems":1,"items":[{"type":"string"}],"maxItems":1,"type":"object"}}} \ No newline at end of file diff --git a/test/Spec.hs b/test/Spec.hs new file mode 100644 index 0000000..8aefe96 --- /dev/null +++ b/test/Spec.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE OverloadedStrings #-} +module Main (main) where + +import Lib (app) +import Test.Hspec +import Test.Hspec.Wai +import Test.Hspec.Wai.JSON + +main :: IO () +main = hspec spec + +spec :: Spec +spec = with (return app) $ do + describe "GET /users" $ do + it "responds with 200" $ do + get "/users" `shouldRespondWith` 200 + it "responds with [User]" $ do + let users = "[{\"userId\":1,\"userFirstName\":\"Isaac\",\"userLastName\":\"Newton\"},{\"userId\":2,\"userFirstName\":\"Albert\",\"userLastName\":\"Einstein\"}]" + get "/users" `shouldRespondWith` users