packages feed

restman-0.7.3.0: test/LibSpec.hs

{-# language OverloadedStrings #-}
module LibSpec (tests) where

import Data.Maybe (isJust, isNothing)

import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as Text

import Test.Tasty
import Test.Tasty.HUnit

import qualified Network.HTTP.Client as HC

import Brick.Types (CursorLocation(..), Location(..))
import qualified Data.CaseInsensitive as CI

import Graphics.Vty.Attributes
  (Attr(attrStyle), MaybeDefault(..), Style, currentAttr, reverseVideo)
import qualified Graphics.Vty.Attributes as VA
import Graphics.Vty.Image (charFill, emptyImage, imageHeight, imageWidth)

import HTTP.Client (UseDefaultHeaders(..))
import Lib
  ( chooseCursor
  , cleanPayload
  , focusAttr
  , focusStyle
  , growWith
  , lbsToText
  , moveFocusNext
  , moveFocusPrev
  , noSyntaxFound
  , splitExtraSpace
  , syntaxHighlightResponse
  )
import Types
  ( AppR(..)
  , AppS(..)
  , CustomHeaderColumn(..)
  , CustomHeaderR(..)
  , RangeInAlign(..)
  )
import UI (mkInitialState)

tests :: TestTree
tests = testGroup "Lib"
  [ splitExtraSpaceTests
  , cleanPayloadTests
  , lbsToTextTests
  , focusStyleTests
  , focusAttrTests
  , moveFocusNextTests
  , moveFocusPrevTests
  , moveFocusNextCustomHeaderTests
  , moveFocusPrevCustomHeaderTests
  , growSpacesTests
  , chooseCursorTests
  , syntaxHighlightResponseTests
  ]

-- ---------------------------------------------------------------------------
-- splitExtraSpace
-- ---------------------------------------------------------------------------

splitExtraSpaceTests :: TestTree
splitExtraSpaceTests = testGroup "splitExtraSpace"
  [ testCase "Min 0 = (0, 0)" $
      splitExtraSpace Min 0 @?= (0, 0)
  , testCase "Min 1 = (0, 1)" $
      splitExtraSpace Min 1 @?= (0, 1)
  , testCase "Min 5 = (0, 5)" $
      splitExtraSpace Min 5 @?= (0, 5)
  , testCase "Min: left is always 0" $
      fst (splitExtraSpace Min 10) @?= 0
  , testCase "Max 0 = (0, 0)" $
      splitExtraSpace Max 0 @?= (0, 0)
  , testCase "Max 1 = (1, 0)" $
      splitExtraSpace Max 1 @?= (1, 0)
  , testCase "Max 5 = (5, 0)" $
      splitExtraSpace Max 5 @?= (5, 0)
  , testCase "Max: right is always 0" $
      snd (splitExtraSpace Max 10) @?= 0
  , testCase "MidL 0 = (0, 0)" $
      splitExtraSpace MidL 0 @?= (0, 0)
  , testCase "MidL 1 = (0, 1)" $
      splitExtraSpace MidL 1 @?= (0, 1)
  , testCase "MidL 2 = (1, 1)" $
      splitExtraSpace MidL 2 @?= (1, 1)
  , testCase "MidL 3 = (1, 2) (left gets the smaller half)" $
      splitExtraSpace MidL 3 @?= (1, 2)
  , testCase "MidL: left + right = n (even)" $
      let (l, r) = splitExtraSpace MidL 6 in l + r @?= 6
  , testCase "MidL: left + right = n (odd)" $
      let (l, r) = splitExtraSpace MidL 7 in l + r @?= 7
  , testCase "MidG 0 = (0, 0)" $
      splitExtraSpace MidG 0 @?= (0, 0)
  , testCase "MidG 1 = (1, 0)" $
      splitExtraSpace MidG 1 @?= (1, 0)
  , testCase "MidG 2 = (1, 1)" $
      splitExtraSpace MidG 2 @?= (1, 1)
  , testCase "MidG 3 = (2, 1) (left gets the larger half)" $
      splitExtraSpace MidG 3 @?= (2, 1)
  , testCase "MidG: left + right = n (even)" $
      let (l, r) = splitExtraSpace MidG 6 in l + r @?= 6
  , testCase "MidG: left + right = n (odd)" $
      let (l, r) = splitExtraSpace MidG 7 in l + r @?= 7
  , testCase "MidL and MidG are symmetric for even n" $
      splitExtraSpace MidL 8 @?= splitExtraSpace MidG 8
  , testCase "MidL and MidG swap halves for odd n" $
      let (lL, rL) = splitExtraSpace MidL 5
          (lG, rG) = splitExtraSpace MidG 5
      in (lL, rL) @?= (rG, lG)
  ]

-- ---------------------------------------------------------------------------
-- cleanPayload
-- ---------------------------------------------------------------------------

cleanPayloadTests :: TestTree
cleanPayloadTests = testGroup "cleanPayload"
  [ testCase "empty input gives empty output" $
      cleanPayload LBS.empty @?= Text.empty
  , testCase "plain ASCII is unchanged" $
      cleanPayload "hello world" @?= "hello world"
  , testCase "ESC is stripped" $
      cleanPayload "\ESCfoo" @?= "foo"
  , testCase "multiple ESC characters are all stripped" $
      cleanPayload "\ESCa\ESCb" @?= "ab"
  , testCase "vertical tab is stripped" $
      cleanPayload "\vfoo" @?= "foo"
  , testCase "tab is expanded to 4 spaces" $
      cleanPayload "\thello" @?= "    hello"
  , testCase "multiple tabs each expand to 4 spaces" $
      cleanPayload "\t\t" @?= "        "
  , testCase "ESC stripped before tab is expanded" $
      cleanPayload "\ESC\tfoo" @?= "    foo"
  , testCase "newlines are preserved" $
      cleanPayload "line1\nline2" @?= "line1\nline2"
  , testCase "carriage return is preserved" $
      cleanPayload "foo\rbar" @?= "foo\rbar"
  , testCase "only special chars gives empty output" $
      cleanPayload "\ESC\v" @?= Text.empty
  , testCase "ESC and vtab together are both stripped" $
      cleanPayload "\ESC\vhello" @?= "hello"
  , testCase "tab followed by ESC: tab expands, ESC stripped" $
      cleanPayload "\t\ESC" @?= "    "
  ]

-- ---------------------------------------------------------------------------
-- lbsToText
-- ---------------------------------------------------------------------------

lbsToTextTests :: TestTree
lbsToTextTests = testGroup "lbsToText"
  [ testCase "empty bytes give empty text" $
      lbsToText LBS.empty @?= Text.empty
  , testCase "ASCII roundtrips" $
      lbsToText "hello" @?= "hello"
  , testCase "UTF-8 multi-byte sequence decodes correctly" $
      -- 0xC3 0xA9 is the UTF-8 encoding of U+00E9 (é)
      lbsToText "\xc3\xa9" @?= "\xe9"
  , testCase "invalid UTF-8 decodes leniently to replacement character" $
      -- 0xFF is never a valid UTF-8 byte; lenientDecode substitutes U+FFFD
      lbsToText "\xff" @?= "\xfffd"
  ]

-- ---------------------------------------------------------------------------
-- focusStyle
-- ---------------------------------------------------------------------------

focusStyleTests :: TestTree
focusStyleTests = testGroup "focusStyle"
  [ testCase "focusStyle 0 = reverseVideo" $
      focusStyle (0 :: Style) @?= reverseVideo
  , testCase "focusStyle reverseVideo = 0" $
      focusStyle reverseVideo @?= (0 :: Style)
  , testCase "applying twice is identity (involutory)" $
      focusStyle (focusStyle (0 :: Style)) @?= (0 :: Style)
  , testCase "applying twice to reverseVideo is identity" $
      focusStyle (focusStyle reverseVideo) @?= reverseVideo
  ]

-- ---------------------------------------------------------------------------
-- focusAttr
-- ---------------------------------------------------------------------------

focusAttrTests :: TestTree
focusAttrTests = testGroup "focusAttr"
  [ testCase "KeepCurrent style becomes SetTo reverseVideo" $
      -- currentAttr has KeepCurrent for all fields
      attrStyle (focusAttr currentAttr) @?= SetTo reverseVideo
  , testCase "Default style becomes SetTo reverseVideo" $
      attrStyle (focusAttr currentAttr { attrStyle = Default }) @?= SetTo reverseVideo
  , testCase "SetTo 0 becomes SetTo reverseVideo" $
      attrStyle (focusAttr currentAttr { attrStyle = SetTo 0 }) @?= SetTo reverseVideo
  , testCase "SetTo reverseVideo becomes SetTo 0" $
      attrStyle (focusAttr currentAttr { attrStyle = SetTo reverseVideo }) @?= SetTo (0 :: Style)
  , testCase "non-style fields are unchanged" $
      let a = focusAttr currentAttr
      in (attrStyle a == SetTo reverseVideo) @?= True
  ]

-- ---------------------------------------------------------------------------
-- moveFocusNext (focus cycle with no custom headers)
-- ---------------------------------------------------------------------------

moveFocusNextTests :: TestTree
moveFocusNextTests = testGroup "moveFocusNext"
  [ testCase "MethodEditor -> UrlEditor" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = base mgr UrlEditor  -- start at UrlEditor, override to MethodEditor
            $ mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing
      focus (moveFocusNext s) @?= UrlEditor
  , testCase "UrlEditor -> DefaultHeadersToggle" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      -- mkInitialState starts focus at UrlEditor
      let s = mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing
      focus (moveFocusNext s) @?= DefaultHeadersToggle
  , testCase "DefaultHeadersToggle -> AddCustomHeader (no headers)" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus DefaultHeadersToggle
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusNext s) @?= AddCustomHeader
  , testCase "AddCustomHeader -> ResponseBodyView" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus AddCustomHeader
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusNext s) @?= ResponseBodyView
  , testCase "ResponseBodyView -> MethodEditor" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus ResponseBodyView
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusNext s) @?= MethodEditor
  , testCase "MethodSelector -> MethodEditor (invalid focus)" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus MethodSelector
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusNext s) @?= MethodEditor
  ]
 where
  withFocus f s = s { focus = f }
  base _mgr _f = withFocus MethodEditor

-- ---------------------------------------------------------------------------
-- moveFocusPrev (focus cycle with no custom headers)
-- ---------------------------------------------------------------------------

moveFocusPrevTests :: TestTree
moveFocusPrevTests = testGroup "moveFocusPrev"
  [ testCase "MethodEditor -> ResponseBodyView" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus MethodEditor
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusPrev s) @?= ResponseBodyView
  , testCase "UrlEditor -> MethodEditor" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      -- mkInitialState starts focus at UrlEditor
      let s = mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing
      focus (moveFocusPrev s) @?= MethodEditor
  , testCase "DefaultHeadersToggle -> UrlEditor" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus DefaultHeadersToggle
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusPrev s) @?= UrlEditor
  , testCase "AddCustomHeader -> DefaultHeadersToggle (no headers)" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus AddCustomHeader
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusPrev s) @?= DefaultHeadersToggle
  , testCase "ResponseBodyView -> AddCustomHeader" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus ResponseBodyView
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusPrev s) @?= AddCustomHeader
  , testCase "MethodSelector -> ResponseBodyView (invalid focus)" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = withFocus MethodSelector
                (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
      focus (moveFocusPrev s) @?= ResponseBodyView
  ]
 where
  withFocus f s = s { focus = f }

-- ---------------------------------------------------------------------------
-- moveFocusNext / moveFocusPrev with custom headers
-- ---------------------------------------------------------------------------

-- One fake header
oneHeader :: [(Bool, (CI.CI BS.ByteString, BS.ByteString))]
oneHeader = [(True, (CI.mk "X-Test", "value"))]

-- Two fake headers
twoHeaders :: [(Bool, (CI.CI BS.ByteString, BS.ByteString))]
twoHeaders = [ (True, (CI.mk "X-First", "one"))
             , (True, (CI.mk "X-Second", "two"))
             ]

moveFocusNextCustomHeaderTests :: TestTree
moveFocusNextCustomHeaderTests = testGroup "moveFocusNext (custom headers)"
  [ testCase "DefaultHeadersToggle -> ExistingCustomHeader 0 ActiveToggle (with 1 header)" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = DefaultHeadersToggle, customHeaders = oneHeader }
      focus (moveFocusNext s) @?=
        ExistingCustomHeader (MkCustomHeaderR { listIndex = 0, column = ActiveToggle })
  , testCase "ExistingCustomHeader 0 ActiveToggle -> NameEditor" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = ExistingCustomHeader (MkCustomHeaderR 0 ActiveToggle)
                , customHeaders = oneHeader }
      focus (moveFocusNext s) @?=
        ExistingCustomHeader (MkCustomHeaderR { listIndex = 0, column = NameEditor })
  , testCase "ExistingCustomHeader 0 NameEditor -> ValueEditor" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = ExistingCustomHeader (MkCustomHeaderR 0 NameEditor)
                , customHeaders = oneHeader }
      focus (moveFocusNext s) @?=
        ExistingCustomHeader (MkCustomHeaderR { listIndex = 0, column = ValueEditor })
  , testCase "ExistingCustomHeader 0 ValueEditor (last header) -> AddCustomHeader" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = ExistingCustomHeader (MkCustomHeaderR 0 ValueEditor)
                , customHeaders = oneHeader }
      focus (moveFocusNext s) @?= AddCustomHeader
  , testCase "ExistingCustomHeader 0 ValueEditor (2 headers) -> next header ActiveToggle" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = ExistingCustomHeader (MkCustomHeaderR 0 ValueEditor)
                , customHeaders = twoHeaders }
      focus (moveFocusNext s) @?=
        ExistingCustomHeader (MkCustomHeaderR { listIndex = 1, column = ActiveToggle })
  ]

moveFocusPrevCustomHeaderTests :: TestTree
moveFocusPrevCustomHeaderTests = testGroup "moveFocusPrev (custom headers)"
  [ testCase "AddCustomHeader -> ExistingCustomHeader 0 ValueEditor (1 header)" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = AddCustomHeader, customHeaders = oneHeader }
      focus (moveFocusPrev s) @?=
        ExistingCustomHeader (MkCustomHeaderR { listIndex = 0, column = ValueEditor })
  , testCase "ExistingCustomHeader 0 NameEditor -> ActiveToggle" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = ExistingCustomHeader (MkCustomHeaderR 0 NameEditor)
                , customHeaders = oneHeader }
      focus (moveFocusPrev s) @?=
        ExistingCustomHeader (MkCustomHeaderR { listIndex = 0, column = ActiveToggle })
  , testCase "ExistingCustomHeader 0 ValueEditor -> NameEditor" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = ExistingCustomHeader (MkCustomHeaderR 0 ValueEditor)
                , customHeaders = oneHeader }
      focus (moveFocusPrev s) @?=
        ExistingCustomHeader (MkCustomHeaderR { listIndex = 0, column = NameEditor })
  , testCase "ExistingCustomHeader 0 ActiveToggle (first header) -> DefaultHeadersToggle" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = ExistingCustomHeader (MkCustomHeaderR 0 ActiveToggle)
                , customHeaders = oneHeader }
      focus (moveFocusPrev s) @?= DefaultHeadersToggle
  , testCase "ExistingCustomHeader 1 ActiveToggle -> previous header ValueEditor" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { focus = ExistingCustomHeader (MkCustomHeaderR 1 ActiveToggle)
                , customHeaders = twoHeaders }
      focus (moveFocusPrev s) @?=
        ExistingCustomHeader (MkCustomHeaderR { listIndex = 0, column = ValueEditor })
  ]

-- ---------------------------------------------------------------------------
-- growWith / growSpaces
-- ---------------------------------------------------------------------------

growSpacesTests :: TestTree
growSpacesTests = testGroup "growSpaces"
  [ testCase "grow emptyImage to 5 wide 3 tall: width == 5" $ do
      let img = growWith (charFill VA.currentAttr ' ') 5 Min 3 Min emptyImage
      imageWidth img @?= 5
  , testCase "grow emptyImage to 5 wide 3 tall: height == 3" $ do
      let img = growWith (charFill VA.currentAttr ' ') 5 Min 3 Min emptyImage
      imageHeight img @?= 3
  , testCase "grow to 0 wide 0 tall is a no-op on emptyImage" $ do
      let img = growWith (charFill VA.currentAttr ' ') 0 Min 0 Min emptyImage
      imageWidth img @?= 0
      imageHeight img @?= 0
  , testCase "image already wider than min: width unchanged" $ do
      let src = charFill VA.currentAttr '*' 10 1
          img = growWith (charFill VA.currentAttr ' ') 5 Min 1 Min src
      imageWidth img @?= 10
  , testCase "image already taller than min: height unchanged" $ do
      let src = charFill VA.currentAttr '*' 1 10
          img = growWith (charFill VA.currentAttr ' ') 1 Min 5 Min src
      imageHeight img @?= 10
  , testCase "Max wide alignment: grows to min width" $ do
      let img = growWith (charFill VA.currentAttr ' ') 10 Max 1 Min (charFill VA.currentAttr '*' 2 1)
      imageWidth img @?= 10
  , testCase "Max vert alignment: grows to min height" $ do
      let img = growWith (charFill VA.currentAttr ' ') 1 Min 5 Max (charFill VA.currentAttr '*' 1 2)
      imageHeight img @?= 5
  , testCase "MidL wide alignment: grows to min width" $ do
      let img = growWith (charFill VA.currentAttr ' ') 6 MidL 1 Min (charFill VA.currentAttr '*' 2 1)
      imageWidth img @?= 6
  , testCase "MidG wide alignment: grows to min width" $ do
      let img = growWith (charFill VA.currentAttr ' ') 6 MidG 1 Min (charFill VA.currentAttr '*' 2 1)
      imageWidth img @?= 6
  ]

-- ---------------------------------------------------------------------------
-- chooseCursor
-- ---------------------------------------------------------------------------

chooseCursorTests :: TestTree
chooseCursorTests = testGroup "chooseCursor"
  [ testCase "no overlay, empty cursor list -> Nothing" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing
      assertBool "expected Nothing" (isNothing (chooseCursor s []))
  , testCase "no overlay, UrlEditor cursor in list -> returns it" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing
          loc = CursorLocation (Location (0, 0)) (Just UrlEditor) True
          result = chooseCursor s [loc]
      assertBool "expected Just" (isJust result)
  , testCase "no overlay, wrong name cursor -> Nothing" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing
          loc = CursorLocation (Location (0, 0)) (Just MethodEditor) True
      assertBool "expected Nothing" (isNothing (chooseCursor s [loc]))
  , testCase "overlay present, empty cursor list -> Nothing" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { overlayState = Just undefined }
      assertBool "expected Nothing" (isNothing (chooseCursor s []))
  , testCase "overlay present, MethodSelector cursor -> returns it" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { overlayState = Just undefined }
          loc = CursorLocation (Location (0, 0)) (Just MethodSelector) True
          result = chooseCursor s [loc]
      assertBool "expected Just" (isJust result)
  , testCase "overlay present, non-MethodSelector cursor -> Nothing" $ do
      mgr <- HC.newManager HC.defaultManagerSettings
      let s = (mkInitialState mgr "GET" ReplaceDefaultHeaders [] Nothing Nothing)
                { overlayState = Just undefined }
          loc = CursorLocation (Location (0, 0)) (Just UrlEditor) True
      assertBool "expected Nothing" (isNothing (chooseCursor s [loc]))
  ]

-- ---------------------------------------------------------------------------
-- syntaxHighlightResponse / noSyntaxFound
-- ---------------------------------------------------------------------------

syntaxHighlightResponseTests :: TestTree
syntaxHighlightResponseTests = testGroup "syntaxHighlightResponse"
  [ testCase "Nothing payload -> non-empty 'no response body' image" $ do
      let img = syntaxHighlightResponse Nothing Nothing
      assertBool "expected non-empty image" (imageWidth img > 0 || imageHeight img > 0)
  , testCase "Just payload, Nothing content-type -> non-empty image" $ do
      let img = syntaxHighlightResponse (Just "hello world") Nothing
      assertBool "expected non-empty image" (imageWidth img > 0 || imageHeight img > 0)
  , testCase "Just payload, Just application/json content-type -> non-empty image" $ do
      let img = syntaxHighlightResponse (Just "{\"a\":1}") (Just "application/json")
      assertBool "expected non-empty image" (imageWidth img > 0 || imageHeight img > 0)
  , testCase "Just payload, Just unknown content-type -> same height as noSyntaxFound" $ do
      let img = syntaxHighlightResponse (Just "data") (Just "application/x-unknown-restman-test")
          img2 = noSyntaxFound "data"
      imageHeight img @?= imageHeight img2
  , testCase "Just payload, Just text/plain content-type -> non-empty image" $ do
      let img = syntaxHighlightResponse (Just "hello world") (Just "text/plain")
      assertBool "expected non-empty image" (imageWidth img > 0 || imageHeight img > 0)
  , testCase "Just payload, Just text/html content-type -> non-empty image" $ do
      let img = syntaxHighlightResponse (Just "<p>hello</p>") (Just "text/html")
      assertBool "expected non-empty image" (imageWidth img > 0 || imageHeight img > 0)
  , testCase "noSyntaxFound empty text -> non-empty image (header line)" $ do
      let img = noSyntaxFound Text.empty
      assertBool "expected non-empty image" (imageWidth img > 0 || imageHeight img > 0)
  , testCase "noSyntaxFound single-line text -> height is 2 (header + 1 line)" $ do
      let img = noSyntaxFound "hello world"
      imageHeight img @?= 2
  , testCase "noSyntaxFound two-line text -> height is 3 (header + 2 lines)" $ do
      let img = noSyntaxFound "line1\nline2"
      imageHeight img @?= 3
  ]