packages feed

hurl-workbench-core-0.1.0.0: test/HurlWorkbench/Workflow/WorkflowTest.hs

module HurlWorkbench.Workflow.WorkflowTest (tests) where

import Data.ByteString qualified as ByteString
import Data.Generics.Labels ()
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text.Encoding
import Data.Text.IO qualified as Text.IO
import Data.Version (makeVersion)
import HurlWorkbench.Hurl.Format
import HurlWorkbench.Prelude
import HurlWorkbench.Workflow.Render
import HurlWorkbench.Workflow.Resolve
import HurlWorkbench.Workspace.Context
import HurlWorkbench.Workspace.Discover (WorkspaceSource (..))
import HurlWorkbench.Workspace.Error (renderWorkspaceError)
import HurlWorkbench.Workspace.Types
import HurlWorkbench.Workspace.Validate
import System.Directory
  ( canonicalizePath,
    doesFileExist,
    getPermissions,
    removeFile,
    setOwnerExecutable,
    setPermissions,
  )
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "workflow"
    [ resolutionTests,
      renderingTests,
      hurlfmtTests
    ]

resolutionTests :: TestTree
resolutionTests =
  testGroup
    "resolution"
    [ testCase "resolves declared fragments in workflow order" $
        withSystemTempDirectory "hurl-workbench-resolve" $ \dir -> do
          validated <- makeWorkspace dir [("first", "first.hurl", "GET https://first.test"), ("second", "second.hurl", "GET https://second.test")] [("ordered", ["second", "first"])]
          case resolveWorkflow validated (WorkflowName "ordered") of
            Left err -> assertFailure (Text.unpack (renderWorkflowError err))
            Right resolved ->
              map (view (#fragment . #name)) (NonEmpty.toList (resolved ^. #sourceFragments))
                @?= [FragmentName "second", FragmentName "first"],
      testCase "reports an unknown workflow without a partial lookup" $
        withSystemTempDirectory "hurl-workbench-resolve" $ \dir -> do
          validated <- makeWorkspace dir [("one", "one.hurl", "GET https://one.test")] [("known", ["one"])]
          resolveWorkflow validated (WorkflowName "missing") @?= Left (WorkflowNotFound (WorkflowName "missing"))
    ]

renderingTests :: TestTree
renderingTests =
  testGroup
    "rendering"
    [ testCase "preserves bytes and inserts only required line feeds" $
        withSystemTempDirectory "hurl-workbench-render" $ \dir -> do
          let first = Text.Encoding.encodeUtf8 "GET https://first.test\r\n# café"
              second = "POST https://second.test\n"
              third = "PUT https://third.test\n\n"
              fourth = "DELETE https://fourth.test"
          validated <-
            makeWorkspace
              dir
              [("first", "first.hurl", first), ("second", "second.hurl", second), ("third", "third.hurl", third), ("fourth", "fourth.hurl", fourth)]
              [("flow", ["first", "second", "third", "fourth"])]
          rendered <- renderNamed validated "flow"
          rendered ^. #contents
            @?= Text.Encoding.decodeUtf8 first
              <> "\n\n"
              <> Text.Encoding.decodeUtf8 second
              <> "\n"
              <> Text.Encoding.decodeUtf8 third
              <> Text.Encoding.decodeUtf8 fourth
              <> "\n"
          map (\span_ -> (span_ ^. #fragmentName, span_ ^. #firstLine, span_ ^. #lastLine)) (NonEmpty.toList (rendered ^. #fragmentSpans))
            @?= [ (FragmentName "first", 1, 2),
                  (FragmentName "second", 4, 4),
                  (FragmentName "third", 6, 7),
                  (FragmentName "fourth", 8, 8)
                ],
      testCase "keeps capture and use entries ordered with placeholders unresolved" $ do
        validated <- fullFixture
        rendered <- renderNamed validated "list-properties"
        let output = rendered ^. #contents
            (_beforeCapture, fromCapture) = Text.breakOn "access_token: jsonpath" output
            (_beforeUse, fromUse) = Text.breakOn "Authorization: Bearer {{access_token}}" output
        assertBool "contains the OAuth capture" (not (Text.null fromCapture))
        assertBool "contains the unresolved use" (not (Text.null fromUse))
        assertBool "capture precedes use" (Text.length fromCapture > Text.length fromUse),
      testCase "rejects an empty fragment" $
        withSystemTempDirectory "hurl-workbench-render" $ \dir -> do
          validated <- makeWorkspace dir [("empty", "empty.hurl", "")] [("flow", ["empty"])]
          resolved <- resolveNamed validated "flow"
          renderWorkflow resolved >>= \case
            Left (EmptyFragment (FragmentName "empty") _) -> pure ()
            other -> assertFailure ("expected EmptyFragment, got " <> show other),
      testCase "reports the invalid UTF-8 byte offset" $
        withSystemTempDirectory "hurl-workbench-render" $ \dir -> do
          validated <- makeWorkspace dir [("bad", "bad.hurl", ByteString.pack [0x47, 0x45, 0xFF, 0x54])] [("flow", ["bad"])]
          resolved <- resolveNamed validated "flow"
          renderWorkflow resolved >>= \case
            Left (InvalidFragmentUtf8 (FragmentName "bad") _ 2 _) -> pure ()
            other -> assertFailure ("expected byte offset 2, got " <> show other),
      testCase "turns a post-validation filesystem change into a render error" $
        withSystemTempDirectory "hurl-workbench-render" $ \dir -> do
          validated <- makeWorkspace dir [("gone", "gone.hurl", "GET https://gone.test")] [("flow", ["gone"])]
          resolved <- resolveNamed validated "flow"
          removeFile (dir </> "gone.hurl")
          renderWorkflow resolved >>= \case
            Left (FragmentReadFailed (FragmentName "gone") _ _) -> pure ()
            other -> assertFailure ("expected FragmentReadFailed, got " <> show other)
    ]

hurlfmtTests :: TestTree
hurlfmtTests =
  testGroup
    "hurlfmt"
    [ testCase "parses the released Hurlfmt version format" $ do
        parseHurlfmtVersion "hurlfmt 8.0.1" @?= Just (makeVersion [8, 0, 1])
        parseHurlfmtVersion "not a version" @?= Nothing,
      testCase "maps a Hurlfmt line diagnostic to its source fragment and cleans the temporary file" $
        withSystemTempDirectory "hurl-workbench-format" $ \dir -> do
          validated <- makeWorkspace dir [("broken", "broken.hurl", "GET https://example.test\n[Asserts")] [("flow", ["broken"])]
          rendered <- renderNamed validated "flow"
          executable <- writeFailingHurlfmt dir
          let capabilities = HurlfmtCapabilities (HurlfmtExecutable executable) (makeVersion [8, 0, 1])
          validateRenderedWorkflow capabilities rendered >>= \case
            Left (InvalidHurl (WorkflowName "flow") (Just span_) diagnostics) -> do
              span_ ^. #fragmentName @?= FragmentName "broken"
              assertBool "keeps Hurlfmt diagnostics" ("expecting" `Text.isInfixOf` diagnostics)
            other -> assertFailure ("expected mapped InvalidHurl, got " <> show other)
          temporaryPath <- Text.unpack . Text.strip <$> Text.IO.readFile (executable <> ".called")
          remains <- doesFileExist temporaryPath
          assertBool "temporary Hurl source was removed" (not remains),
      testCase "validates every workflow in stable name order" $
        withSystemTempDirectory "hurl-workbench-format" $ \dir -> do
          validated <-
            makeWorkspace
              dir
              [("broken", "broken.hurl", "GET https://example.test\n[Asserts")]
              [("zeta", ["broken"]), ("alpha", ["broken"])]
          executable <- writeFailingHurlfmt dir
          let capabilities = HurlfmtCapabilities (HurlfmtExecutable executable) (makeVersion [8, 0, 1])
          validateWorkspaceSyntax capabilities validated >>= \case
            Right issues -> map syntaxWorkflowName issues @?= [Just (WorkflowName "alpha"), Just (WorkflowName "zeta")]
            Left err -> assertFailure (Text.unpack (renderDependencyError err)),
      testCase "reports an executable that disappears after detection" $
        withSystemTempDirectory "hurl-workbench-format" $ \dir -> do
          validated <- makeWorkspace dir [("one", "one.hurl", "GET https://one.test")] [("flow", ["one"])]
          rendered <- renderNamed validated "flow"
          let missing = dir </> "missing-hurlfmt"
          validateRenderedWorkflow (HurlfmtCapabilities (HurlfmtExecutable missing) (makeVersion [8, 0, 1])) rendered
            >>= (@?= Left (HurlfmtNotFound missing))
    ]

syntaxWorkflowName :: WorkflowSyntaxError -> Maybe WorkflowName
syntaxWorkflowName = \case
  SyntaxFormatFailed (InvalidHurl name _span _diagnostics) -> Just name
  SyntaxRenderFailed name _err -> Just name
  SyntaxResolutionFailed _err -> Nothing
  SyntaxFormatFailed _err -> Nothing

makeWorkspace :: FilePath -> [(Text, FilePath, ByteString.ByteString)] -> [(Text, [Text])] -> IO ValidatedWorkspace
makeWorkspace directory fragmentInputs workflowInputs = do
  canonicalRoot <- canonicalizePath directory
  for_ fragmentInputs $ \(_name, path, bytes) -> ByteString.writeFile (canonicalRoot </> path) bytes
  let decoded =
        Workspace
          { schemaVersion = supportedSchemaVersion,
            parameters = [],
            fragments = [Fragment (FragmentName name) path Nothing | (name, path, _bytes) <- fragmentInputs],
            workflows =
              [ Workflow
                  { name = WorkflowName name,
                    fragments = map FragmentName fragmentNames,
                    parameters = [],
                    description = Nothing
                  }
              | (name, fragmentNames) <- workflowInputs
              ],
            recipes = [],
            matrices = [],
            checks = [],
            services = [],
            suites = []
          }
      context =
        WorkspaceContext
          { manifestPath = canonicalRoot </> "hurl-workbench.dhall",
            workspaceRoot = WorkspaceRoot canonicalRoot,
            workspace = decoded
          }
  validateWorkspace context >>= \case
    Left issues -> assertFailure (Text.unpack (Text.unlines (map renderValidationIssue (toList issues))))
    Right validated -> pure validated

resolveNamed :: ValidatedWorkspace -> Text -> IO ResolvedWorkflow
resolveNamed validated name =
  case resolveWorkflow validated (WorkflowName name) of
    Left err -> assertFailure (Text.unpack (renderWorkflowError err))
    Right resolved -> pure resolved

renderNamed :: ValidatedWorkspace -> Text -> IO RenderedWorkflow
renderNamed validated name = do
  resolved <- resolveNamed validated name
  renderWorkflow resolved >>= \case
    Left err -> assertFailure (Text.unpack (renderRenderError err))
    Right rendered -> pure rendered

fullFixture :: IO ValidatedWorkspace
fullFixture = do
  context <-
    loadWorkspaceContext (ExplicitWorkspace "test/fixtures/workspaces/full/hurl-workbench.dhall") >>= \case
      Left err -> assertFailure (Text.unpack (renderWorkspaceError err))
      Right loaded -> pure loaded
  validateWorkspace context >>= \case
    Left issues -> assertFailure (Text.unpack (Text.unlines (map renderValidationIssue (toList issues))))
    Right validated -> pure validated

writeFailingHurlfmt :: FilePath -> IO FilePath
writeFailingHurlfmt directory = do
  let executable = directory </> "hurlfmt-failure"
  Text.IO.writeFile
    executable
    ( Text.unlines
        [ "#!/bin/sh",
          "last=''",
          "for arg in \"$@\"; do last=\"$arg\"; done",
          "printf '%s\\n' \"$last\" > \"$0.called\"",
          "printf 'error: Parsing JSON\\n  --> %s:2:3\\n   |\\n 2 | [Asserts\\n   |   ^ expecting a closing bracket\\n' \"$last\" >&2",
          "exit 1"
        ]
    )
  permissions <- getPermissions executable
  setPermissions executable (setOwnerExecutable True permissions)
  pure executable