diff --git a/PaymentServer.cabal b/PaymentServer.cabal index 89f755458e41e87cfe74c2ed2247fff9871f7fd6..0cc0fb6fc71b823d4353fd4deacbaf8adfd9295b 100644 --- a/PaymentServer.cabal +++ b/PaymentServer.cabal @@ -21,6 +21,7 @@ library , servant-server , wai , warp + , stripe-core default-language: Haskell2010 executable PaymentServer-exe @@ -41,6 +42,14 @@ test-suite PaymentServer-test , hspec-wai , hspec-wai-json , aeson + , stripe-core + , time + , QuickCheck + , quickcheck-instances + , wai-extra + , bytestring + , text + , http-types ghc-options: -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010 diff --git a/src/Lib.hs b/src/Lib.hs index 3844204aadfc686548ee94661bb696ead5d67e35..996746be98fac078e6d13f8e940ba08d0ea3d487 100644 --- a/src/Lib.hs +++ b/src/Lib.hs @@ -12,15 +12,8 @@ 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] +type API = + "webhook" :> Post '[JSON] () startApp :: IO () startApp = run 8080 app @@ -32,9 +25,4 @@ api :: Proxy API api = Proxy server :: Server API -server = return users - -users :: [User] -users = [ User 1 "Isaac" "Newton" - , User 2 "Albert" "Einstein" - ] +server = return () diff --git a/stack.yaml b/stack.yaml index 9879a51389f8ae856386953ccd3db2a4ea146b24..75399a2e58530adf91ef7986d22afc5aff66d297 100644 --- a/stack.yaml +++ b/stack.yaml @@ -37,7 +37,8 @@ packages: # Dependency packages to be pulled from upstream that are not in the resolver # using the same syntax as the packages field. # (e.g., acme-missiles-0.3) -# extra-deps: [] +extra-deps: + - "stripe-core-2.5.0" # Override default flag values for local packages and extra-deps # flags: {} diff --git a/test/Spec.hs b/test/Spec.hs index 8aefe968b9a875f97786a3ab87cdf4bd7dbd20b6..bd0eadfbd184dfee4ac3dcea71ce1c6cdb2e7819 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -2,19 +2,185 @@ {-# LANGUAGE OverloadedStrings #-} module Main (main) where -import Lib (app) +import Lib + ( app + ) + +import Data.ByteString.Lazy as LazyBS +import Data.ByteString as BS + +import Data.Text + ( Text + ) + +import Data.Time.Clock + ( UTCTime + ) + +import Data.Aeson + ( encode + ) + +import Web.Stripe.Types + ( Charge(Charge) + , StatementDescription(StatementDescription) + , MetaData(MetaData) + , Description(Description) + , CustomerId(CustomerId) + , InvoiceId(InvoiceId) + , ChargeId(ChargeId) + , Expandable(Id) + , Currency(UnknownCurrency) + , Amount(Amount) + , StripeList(StripeList, list, totalCount, hasMore) + ) + +import Web.Stripe.Event + ( Event(Event, eventId, eventCreated, eventLiveMode, eventType, eventData, eventObject, eventPendingWebHooks, eventRequest) + , EventType(ChargeSucceededEvent) + , EventData(ChargeEvent) + , EventId(EventId) + ) + import Test.Hspec + ( Spec + , hspec + , describe + , it + ) import Test.Hspec.Wai -import Test.Hspec.Wai.JSON + ( WaiSession + , with + , post + , request + , shouldRespondWith + ) +import Network.Wai.Test + ( SResponse + ) + +import Network.HTTP.Types.Method + ( methodPost + ) + +import Test.QuickCheck + ( Gen + , Arbitrary + , forAll + , property + , arbitrary + ) + +import Test.QuickCheck.Instances.Time + ( -- Get the `Gen UTCTime` instance + ) +import Test.QuickCheck.Instances.Text + ( -- Get the `Gen Text` instance + ) main :: IO () main = hspec spec +-- Post some JSON to a path. +-- Return a function from path to a response +postJSON :: BS.ByteString -> (LazyBS.ByteString -> WaiSession SResponse) +postJSON path = + request methodPost path [("Content-Type", "application/json")] + +instance Arbitrary Charge where + arbitrary = Charge + <$> arbitrary -- chargeId :: ChargeId + <*> arbitrary -- chargeObject :: Text + <*> arbitrary -- chargeCreated :: UTCTime + <*> arbitrary -- chargeLiveMode :: Bool + <*> arbitrary -- chargePaid :: Bool + <*> arbitrary -- chargeAmount :: Amount + <*> (return UnknownCurrency) -- chargeCurrency :: Currency + <*> (return False) -- chargeRefunded :: Bool + <*> (return Nothing) -- chargeCreditCard :: Maybe Card + <*> arbitrary -- chargeCaptured :: Bool + <*> (return mempty) -- chargeRefunds :: StripeList Refund + <*> (return Nothing) -- chargeBalanceTransaction :: Maybe (Expandable TransactionId) + <*> (return Nothing) -- chargeFailureMessage :: Maybe Text + <*> (return Nothing) -- chargeFailureCode :: Maybe Text + <*> (return 0) -- chargeAmountRefunded :: Int + <*> arbitrary -- chargeCustomerId :: Maybe (Expandable CustomerId) + <*> (return Nothing) -- chargeInvoice :: Maybe (Expandable InvoiceId) + <*> arbitrary -- chargeDescription :: Maybe Description + <*> (return Nothing) -- chargeDispute :: Maybe Dispute + <*> arbitrary -- chargeMetaData :: MetaData + <*> arbitrary -- chargeStatementDescription :: Maybe StatementDescription + <*> arbitrary -- chargeReceiptEmail :: Maybe Text + <*> arbitrary -- chargeReceiptNumber :: Maybe Text + + +instance Semigroup (StripeList a) where + -- Very weak implementation + x <> y = StripeList (list x <> list y) "" "" Nothing (hasMore x || hasMore y) + +instance Monoid (StripeList a) where + mempty = StripeList [] "" "" (Just 0) False + +instance Arbitrary Amount where + arbitrary = Amount <$> arbitrary + + +instance Arbitrary EventId where + arbitrary = EventId <$> arbitrary + + +instance Arbitrary StatementDescription where + arbitrary = StatementDescription <$> arbitrary + + +instance Arbitrary MetaData where + arbitrary = MetaData <$> arbitrary + + +instance Arbitrary Description where + arbitrary = Description <$> arbitrary + + +instance Arbitrary InvoiceId where + arbitrary = InvoiceId <$> arbitrary + + +instance Arbitrary ChargeId where + arbitrary = ChargeId <$> arbitrary + + +instance Arbitrary CustomerId where + arbitrary = CustomerId <$> arbitrary + + +instance Arbitrary a => Arbitrary (Expandable a) where + arbitrary = Id <$> arbitrary + + +chargeSucceededEvents :: Gen Event +chargeSucceededEvents = + Event + <$> arbitrary -- eventId + <*> arbitrary -- eventCreated + <*> arbitrary -- eventLiveMode + <*> (return ChargeSucceededEvent) -- eventType + <*> (ChargeEvent + <$> arbitrary -- the charge + ) -- eventData + <*> arbitrary -- eventObject + <*> arbitrary -- eventPendingWebHooks + <*> arbitrary -- eventRequest + 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 + describe "error behavior of POST /webhook" $ do + it "responds to non-JSON Content-Type with 400 (Invalid Request)" $ + post "/webhook" "{}" `shouldRespondWith` 400 + + it "responds to JSON non-Event body with 400 (Invalid Request)" $ + postJSON "/webhook" "{}" `shouldRespondWith` 400 + + -- describe "success behavior of POST /webhook" $ do + -- it "responds to JSON-encoded Event body with 200 (OK)" $ + -- forAll chargeSucceededEvents $ \event -> + -- postJSON "/webhook" (encode event) `shouldRespondWith` 200