{-# 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 :: Int) 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 :: Int) 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 :: Int) 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 :: Int) 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 :: Int) 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 :: Int) 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 "

hello

") (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 ]