packages feed

sensei-0.9.0: test/HTTPSpec.hs

{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wno-ambiguous-fields #-}
module HTTPSpec (spec) where

import           Helper hiding (putStrLn, pending)
import qualified Prelude
import qualified VCR

import           Network.Wai (Application)
import           Test.Hspec.Wai
import           Test.Hspec.Wai.Internal (WaiSession(..))
import           Control.Monad.Trans.State (StateT(..))
import           Control.Monad.Trans.Reader (ReaderT(..))
import qualified Data.ByteString as B

import           Config
import           Config.DeepSeek
import qualified HTTP
import qualified Trigger
import qualified GHC.Diagnostic.Type as Diagnostic

verbose :: Bool
verbose = False

putStrLn :: String -> IO ()
putStrLn
  | verbose = Prelude.putStrLn
  | otherwise = \ _ -> pass

withTape :: VCR.Tape -> WaiSession st a -> WaiSession st a
withTape tape = lift (VCR.with tape)
  where
    lift :: forall st a. (forall b. IO b -> IO b) -> WaiSession st a -> WaiSession st a
    lift f action = WaiSession $ ReaderT \ st -> ReaderT \ application -> StateT \ clientState -> do
      f $ runStateT (runReaderT (runReaderT (unWaiSession action) st) application) clientState

spec :: Spec
spec = do
  describe "app" $ do
    let
      file :: FilePath
      file = "Foo.hs"

      withApp :: (Trigger.Result, String, [Diagnostic]) -> SpecWith (FilePath, Application) -> Spec
      withApp lastResult = around \ item -> withTempDirectory \ dir -> do
        item (dir, HTTP.app putStrLn defaultConfig dir $ return lastResult)

      withAppWithFailure :: FilePath -> SpecWith (FilePath, Application) -> Spec
      withAppWithFailure name = around \ item -> withTempDirectory \ dir -> do
        let
          testCaseDir :: FilePath
          testCaseDir = "test/assets" </> name

          copySource :: IO ()
          copySource = readFile (testCaseDir </> file) >>= writeFile (dir </> file)

          readErrFile :: IO (Maybe Diagnostic)
          readErrFile = fmap normalizeFileName . Diagnostic.parse <$> B.readFile (testCaseDir </> "err.json")

        copySource
        Just err <- readErrFile
        config <- loadConfig

        let
          deepSeek :: Maybe DeepSeek
          deepSeek = config.deepSeek <|> Just (DeepSeek $ BearerToken "")

          app :: Application
          app = HTTP.app putStrLn defaultConfig { deepSeek } dir (return $ (Trigger.Failure, "", [err]))

        item (dir, app)
        where
          normalizeFileName :: Diagnostic -> Diagnostic
          normalizeFileName err = err { span = normalizeSpan <$> err.span }

          normalizeSpan :: Span -> Span
          normalizeSpan span = span { file }

    describe "/" $ do
      context "on success" $ do
        withApp (Trigger.Success, withColor Green "success", []) $ do
          it "returns status 200" $ do
            get "/" `shouldRespondWith` fromString (withColor Green "success")

          context "with ?color" $ do
            it "keeps terminal sequences" $ do
              get "/?color" `shouldRespondWith` fromString (withColor Green "success")

          context "with ?color=true" $ do
            it "keeps terminal sequences" $ do
              get "/?color=true" `shouldRespondWith` fromString (withColor Green "success")

          context "with ?color=false" $ do
            it "removes terminal sequences" $ do
              get "/?color=false" `shouldRespondWith` "success"

          context "with an invalid value for ?color" $ do
            it "returns status 400" $ do
              get "/?color=some%20value" `shouldRespondWith` 400 { matchBody = "invalid value for color: some%20value" }

      context "on failure" $ do
        withApp (Trigger.Failure, withColor Red "failure", []) $ do
          it "return 500" $ do
            get "/" `shouldRespondWith` 500

    describe "/diagnostics" $ do
      let
        start :: Location
        start = Location 23 42

        span :: Maybe Span
        span = Just $ Span "Foo.hs" start start

        err :: Diagnostic
        err = (diagnostic Error) { span, message = ["failure"] }

        expected :: ResponseMatcher
        expected = fromString . decodeUtf8 $ to_json [err]

      withApp (Trigger.Failure, "", [err]) $ do
        it "returns GHC diagnostics" $ do
          get "/diagnostics" `shouldRespondWith` expected

    describe "/quick-fix" $ do
      withAppWithFailure "use-TemplateHaskellQuotes" $ do
        it "applies quick fixes" $ do
          post "/quick-fix" "{}" `shouldRespondWith` "" { matchStatus = 204 }
          dir <- getState
          liftIO $ readFile (dir </> file) `shouldReturn` unlines [
              "{-# LANGUAGE TemplateHaskellQuotes #-}"
            , "module Foo where"
            , "foo = [|23|]"
            ]

      withAppWithFailure "not-in-scope-perhaps-use-one-of-these" $ do
        context "when \"choice\" is specified" $ do
          it "applies the selected solution" $ do
            post "/quick-fix" "{\"choice\":2}" `shouldRespondWith` "" { matchStatus = 204 }
            dir <- getState
            liftIO $ readFile (dir </> file) `shouldReturn` unlines [
                "module Foo where"
              , "foo = foldr"
              ]

    describe "/deep-fix" $ do
      withAppWithFailure "use-TemplateHaskellQuotes" $ do
        it "applies quick fixes using DeepSeek" >>> sequential $ withTape "test/vcr/tape.yaml" do
          post "/deep-fix" "" `shouldRespondWith` "" { matchStatus = 204 }
          dir <- getState
          liftIO $ readFile (dir </> file) `shouldReturn` unlines [
              "{-# LANGUAGE TemplateHaskell #-}"
            , "module Foo where"
            , "foo = [|23|]"
            ]

    context "when querying a non-existing endpoint" $ withApp undefined $ do
      it "returns status 404" $ do
        get "/foo" `shouldRespondWith` 404 {matchBody = "404 Not Found"}

      context "with \"Accept: application/json\"" $ do
        it "returns a JSON error" $ do
          request "GET" "/foo" [("Accept", "application/json")] "" `shouldRespondWith` 404 {
            matchBody = fromString $ intercalate "\n" [
                "{"
              , "  \"title\": \"Not Found\","
              , "  \"status\": 404"
              , "}"
              ]
          }