{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} module Minilang.ServerSpec where import Control.Monad (void) import Data.Aeson (FromJSON, eitherDecode, encode) import Data.ByteString (isInfixOf, toStrict) import qualified Data.ByteString as BS import qualified Data.ByteString.Base64.Lazy as Base64 import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as LBS import Data.Data (Proxy (..)) import Data.Text.Lazy.Encoding (decodeUtf8) import Minilang.API (Challenge (..), Score, User (..), newToken) import Minilang.App (makeEnv) import Minilang.Server (serveAPI) import Network.Wai (Application) import Network.Wai.Test (SResponse) import System.Random (StdGen, mkStdGen64) import Test.Hspec (Spec, it) import Test.Hspec.Wai (WaiSession, get, matchBody, request, shouldRespondWith, with) import Test.Hspec.Wai.Matcher (MatchBody (..)) spec :: Spec spec = with (app testSeed) $ do let token = Base64.encode (fst $ newToken testSeed) alice = encode $ User "alice" it "GET / returns 'index.html'" $ do get "/" `shouldRespondWith` 200 { matchBody = MatchBody $ \_ actual -> if "" `isInfixOf` toStrict actual then Nothing else Just "Expected HTML content" } it "GET /api/leaderboard returns empty list of scores" $ do get "/api/leaderboard" `shouldRespondWith` 200 {matchBody = arrayOfLengthOf (Proxy @Score) 2} it "POST /api/register returns registration token given user is not registered" $ do postJSON "/api/register" alice `shouldRespondWith` 201 { matchBody = MatchBody $ \_ t -> if t == encode (decodeUtf8 token) then Nothing else Just ("Expected token " ++ show token ++ ", but got " ++ show t) } it "POST /api/register returns 400 given user is already registered" $ do void $ postJSON "/api/register" alice postJSON "/api/register" alice `shouldRespondWith` 400 it "GET /api/challenge returns next challenge given user is registered" $ do let expected = encode $ Challenge {level = 0, expr = "x"} postJSON "/api/register" alice `shouldRespondWith` 201 get ("/api/challenge?token=" <> LBS.toStrict token) `shouldRespondWith` 200 { matchBody = MatchBody $ \_ b -> if b == expected then Nothing else Just ("Expected challenge " ++ show expected ++ ", but got " ++ show b) } it "GET /api/challenge returns 400 given user is not registered" $ get ("/api/challenge?token=" <> LBS.toStrict token) `shouldRespondWith` 400 it "POST /api/challenge returns 200 given challenge response is correct" $ do let response = "\"x\"" void $ postJSON "/api/register" alice postJSON ("/api/challenge?token=" <> LBS.toStrict token) response `shouldRespondWith` 200 postJSON :: BS.ByteString -> ByteString -> WaiSession () SResponse postJSON url = request "POST" url [("Content-Type", "application/json")] arrayOfLengthOf :: forall elem proxy. (FromJSON elem) => proxy elem -> Int -> MatchBody arrayOfLengthOf _ len = MatchBody $ \_ actual -> case eitherDecode actual of Right (scores :: [elem]) -> if length scores == len then Nothing else Just $ "Expected array of length " ++ show len ++ ", but got " ++ show (length scores) Left err -> Just $ "Failed to decode JSON: " ++ err app :: StdGen -> IO Application app g = do env <- makeEnv g pure $ serveAPI env testSeed :: StdGen testSeed = mkStdGen64 42