hprox-0.7.0: test/Network/HProx/PureSpec.hs
-- SPDX-License-Identifier: Apache-2.0
--
-- Copyright (C) 2026 Bin Jin. All Rights Reserved.
module Network.HProx.PureSpec
( spec
) where
import Data.List.NonEmpty qualified as NE
import Network.HProx.Log
import Network.HProx.Util
import Test.Hspec
spec :: Spec
spec = do
describe "parseHostPort" $ do
it "parses a valid host and port" $
parseHostPort "example.com:443" `shouldBe` Just ("example.com", 443)
it "uses the last colon as the port separator" $
parseHostPort "example.com:proxy:8080" `shouldBe` Just ("example.com:proxy", 8080)
it "accepts an empty host portion" $
parseHostPort ":123" `shouldBe` Just ("", 123)
it "rejects missing, invalid, trailing, and out-of-range ports" $
map parseHostPort ["example.com", "example.com:http", "example.com:80x", "example.com:0", "example.com:65536", "example.com:"]
`shouldBe` replicate 6 Nothing
describe "parseHostPortWithDefault" $ do
it "returns an explicit port when parsing succeeds" $
parseHostPortWithDefault 80 "example.com:443" `shouldBe` ("example.com", 443)
it "returns the original input and default port when parsing fails" $
parseHostPortWithDefault 80 "example.com:not-a-port" `shouldBe` ("example.com:not-a-port", 80)
describe "passwordReader" $ do
it "parses plaintext password lines" $
passwordReader "user:pass" `shouldBe` Just ("user", PlainText "pass")
it "parses salted password lines by base64-decoding salt and hash" $
passwordReader "user:c2FsdA==:aGFzaA==" `shouldBe` Just ("user", Salted "salt" "hash")
it "rejects invalid salted and over-split password lines" $
map passwordReader ["user:not-base64:also-not-base64", "user:pass:extra:field"]
`shouldBe` [Nothing, Nothing]
describe "passwordWriter" $
it "writes salted password lines with base64 salt and hash" $
passwordWriter "user" (PasswordSalted "salt" "hash") `shouldBe` "user:c2FsdA==:aGFzaA=="
describe "hashPasswordWithSalt" $ do
it "returns a typed failure when Argon2 rejects hashing parameters" $
case hashPasswordWithSalt "" (PlainText "pass") of
Left (PasswordHashError msg) -> msg `shouldContain` "unable to hash password with salt"
Right _ -> expectationFailure "expected typed hash failure"
it "keeps already salted passwords without rehashing" $
hashPasswordWithSalt "" (Salted "salt" "hash") `shouldBe` Right (PasswordSalted "salt" "hash")
describe "logLevelReader" $ do
it "parses all current log levels" $
map logLevelReader ["trace", "debug", "info", "warn", "error", "none"]
`shouldBe` map Just [TRACE, DEBUG, INFO, WARN, ERROR, NONE]
it "rejects unknown log levels and different casing" $
map logLevelReader ["", "warning", "INFO"] `shouldBe` replicate 3 Nothing
describe "parseLogOutput" $
it "preserves current log destination parsing policy" $
map parseLogOutput ["none", "stdout", "stderr", "hprox.log"]
`shouldBe` [LogOutputNone, LogOutputStdout, LogOutputStderr, LogOutputFile "hprox.log"]
describe "splitBy" $ do
it "preserves empty segments between, before, and after separators" $
NE.toList (splitBy ':' ":a::b:") `shouldBe` ["", "a", "", "b", ""]
it "returns one empty segment for empty input" $
NE.toList (splitBy ':' "") `shouldBe` [""]