blob: ef05e209a0b0e05caa4184bb810107fbe3a19882 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
{-# LANGUAGE LambdaCase #-}
module Minilang.IntegrationSpec where
import Control.Exception (bracket)
import Data.Aeson (eitherDecodeFileStrict)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Minilang.API
import Minilang.REPL.Types (Evaluator (..))
import Minilang.Server (Server (..), startServer, stopServer)
import Network.HTTP.Client (defaultManagerSettings, newManager)
import Servant.API ((:<|>) (..))
import Servant.Client hiding (mkClientEnv)
import qualified Servant.Client as SC
import Test.Hspec
-- ---------------------------------------------------------------------------
-- Servant client functions derived from the API type
-- ---------------------------------------------------------------------------
registerClient :: User -> ClientM Token
unregisterClient :: Maybe Token -> ClientM Score
leaderboardClient :: ClientM [Score]
scoreClient :: Maybe Token -> ClientM Score
challengeClient :: Maybe Token -> ClientM Challenge
answerClient :: Maybe Token -> Text -> ClientM ()
registerClient
:<|> unregisterClient
:<|> leaderboardClient
:<|> scoreClient
:<|> challengeClient
:<|> answerClient
:<|> _ =
client (Proxy :: Proxy LambdaAPI)
-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
-- | Build a 'ClientEnv' pointing at localhost on the given port.
mkEnv :: Int -> IO ClientEnv
mkEnv port = do
mgr <- newManager defaultManagerSettings
pure $ SC.mkClientEnv mgr (BaseUrl Http "127.0.0.1" port "")
-- | Run a servant client action, failing the test on a Left.
runC :: ClientEnv -> ClientM a -> IO a
runC env action =
runClientM action env >>= \case
Left err -> fail $ "Servant client error: " <> show err
Right v -> pure v
-- ---------------------------------------------------------------------------
-- Spec
-- ---------------------------------------------------------------------------
spec :: Spec
spec =
around (withServer DeBruijn) $
describe "Full contest integration" $ do
it "Alice can solve all challenges and reach the maximum grade" $
\Server{serverPort} -> do
-- Load the same challenges.json the server loaded so we know the
-- expected final grade.
challenges <-
eitherDecodeFileStrict "challenges.json" >>= \case
Left err -> fail $ "Could not load challenges.json: " <> err
Right cs -> pure (cs :: [Challenge])
let maxGrade = length challenges
env <- mkEnv serverPort
-- Register Alice
token <- runC env $ registerClient (User "alice")
-- Solve every challenge in order
mapM_ (solveChallenge env token) challenges
-- Verify final grade equals number of challenges
Score{grade} <- runC env $ scoreClient (Just token)
grade `shouldBe` maxGrade
-- Clean up
_ <- runC env $ unregisterClient (Just token)
pure ()
-- | Fetch the current challenge for the token, post the known answer, and
-- assert the server accepted it (200 OK / no exception).
solveChallenge :: ClientEnv -> Token -> Challenge -> IO ()
solveChallenge env token Challenge{level, answer} = do
-- Confirm the server gives us the challenge we expect at this grade
Challenge{level = serverLevel} <- runC env $ challengeClient (Just token)
serverLevel `shouldBe` level
-- Post the correct answer
runC env $ answerClient (Just token) answer
-- ---------------------------------------------------------------------------
-- Server lifecycle
-- ---------------------------------------------------------------------------
-- | Spawn the real server (which itself reads challenges.json and picks a
-- free port) and tear it down after the test.
withServer :: Evaluator -> (Server -> IO ()) -> IO ()
withServer evaluator = bracket (startServer 0 evaluator) stopServer
|