packages feed

hprox-0.7.0: test/Network/HProx/ConfigSpec.hs

-- SPDX-License-Identifier: Apache-2.0
--
-- Copyright (C) 2026 Bin Jin. All Rights Reserved.
{-# LANGUAGE CPP #-}

module Network.HProx.ConfigSpec
  ( spec
  ) where

import Control.Exception  (try)
import System.Environment (withArgs)
import System.Exit        (ExitCode(..))

import Network.HProx

import Test.Hspec

spec :: Spec
spec = do
  describe "defaultConfig" $
    it "matches the documented default runtime values" $
      assertDefaultConfig defaultConfig

  describe "getConfig" $ do
    it "uses default configuration when no options are supplied" $ do
      conf <- parseConfig []
      assertDefaultConfig conf

    it "parses long-form options into the current Config fields" $ do
      conf <- parseConfig longOptionArgs
      _bind conf `shouldBe` Just "127.0.0.1"
      _port conf `shouldBe` 8080
      case _ssl conf of
        [(host, cert)] -> do
          host `shouldBe` "example.com"
          certfile cert `shouldBe` "cert.pem"
          keyfile cert `shouldBe` "key.pem"
        value -> expectationFailure $ "unexpected TLS entries: " <> show (length value)
      _auth conf `shouldBe` Just "userpass.txt"
      _ws conf `shouldBe` Just "ws.example:443"
      _rev conf `shouldBe` [(Just "example.com", "/api/", "upstream:80")]
      _doh conf `shouldBe` Just "1.1.1.1:53"
      _hide conf `shouldBe` True
      _naive conf `shouldBe` True
      _name conf `shouldBe` "custom"
      _acme conf `shouldBe` Just "thumbprint"
      _log conf `shouldBe` "stderr"
      _loglevel conf `shouldBe` DEBUG
#ifdef OS_UNIX
      _user conf `shouldBe` Just "nobody"
      _group conf `shouldBe` Just "nogroup"
#endif
#ifdef QUIC_ENABLED
      _quic conf `shouldBe` Just 8443
#endif

    it "parses current short aliases" $ do
      conf <- parseConfig shortOptionArgs
      _bind conf `shouldBe` Just "0.0.0.0"
      _port conf `shouldBe` 8081
      case _ssl conf of
        [(host, cert)] -> do
          host `shouldBe` "short.example"
          certfile cert `shouldBe` "short-cert.pem"
          keyfile cert `shouldBe` "short-key.pem"
        value -> expectationFailure $ "unexpected TLS entries: " <> show (length value)
      _auth conf `shouldBe` Just "short-userpass.txt"
#ifdef OS_UNIX
      _user conf `shouldBe` Just "daemon"
      _group conf `shouldBe` Just "daemon"
#endif
#ifdef QUIC_ENABLED
      _quic conf `shouldBe` Just 9443
#endif

    it "parses reverse proxy option forms" $ do
      remoteOnly <- parseConfig ["--rev", "backend:80"]
      prefixOnly <- parseConfig ["--rev", "/api/backend:80"]
      domainAndPrefix <- parseConfig ["--rev", "//example.com/api/backend:80"]
      _rev remoteOnly `shouldBe` [(Nothing, "/", "backend:80")]
      _rev prefixOnly `shouldBe` [(Nothing, "/api/", "backend:80")]
      _rev domainAndPrefix `shouldBe` [(Just "example.com", "/api/", "backend:80")]

    it "accepts only bounded listener ports" $ do
      low <- parseConfig ["--port", "1"]
      high <- parseConfig ["--port", "65535"]
      _port low `shouldBe` 1
      _port high `shouldBe` 65535
      shouldExitWith ["--port", "0"] (ExitFailure 1)
      shouldExitWith ["--port", "65536"] (ExitFailure 1)
      shouldExitWith ["--port", "-1"] (ExitFailure 1)

#ifdef QUIC_ENABLED
    it "accepts only bounded QUIC ports" $ do
      low <- parseConfig ["--quic", "1"]
      high <- parseConfig ["--quic", "65535"]
      _quic low `shouldBe` Just 1
      _quic high `shouldBe` Just 65535
      shouldExitWith ["--quic", "0"] (ExitFailure 1)
      shouldExitWith ["--quic", "65536"] (ExitFailure 1)
      shouldExitWith ["--quic", "-1"] (ExitFailure 1)
#endif
    it "rejects invalid TLS, reverse proxy, and log-level options" $ do
      shouldExitWith ["--tls", "example.com:cert-only"] (ExitFailure 1)
      shouldExitWith ["--rev", "//example.com"] (ExitFailure 1)
      shouldExitWith ["--rev", "/api/"] (ExitFailure 1)
      shouldExitWith ["--rev", "/"] (ExitFailure 1)
      shouldExitWith ["--rev", "///api/backend:80"] (ExitFailure 1)
      shouldExitWith ["--loglevel", "verbose"] (ExitFailure 1)

    it "exercises help and version parser exits" $ do
      shouldExitWith ["--help"] ExitSuccess
      shouldExitWith ["--version"] ExitSuccess

assertDefaultConfig :: Config -> Expectation
assertDefaultConfig conf = do
  _bind conf `shouldBe` Nothing
  _port conf `shouldBe` 3000
  length (_ssl conf) `shouldBe` 0
  _auth conf `shouldBe` Nothing
  _ws conf `shouldBe` Nothing
  _rev conf `shouldBe` []
  _doh conf `shouldBe` Nothing
  _hide conf `shouldBe` False
  _naive conf `shouldBe` False
  _name conf `shouldBe` "hprox"
  _acme conf `shouldBe` Nothing
  _log conf `shouldBe` "stdout"
  _loglevel conf `shouldBe` INFO
#ifdef OS_UNIX
  _user conf `shouldBe` Nothing
  _group conf `shouldBe` Nothing
#endif
#ifdef QUIC_ENABLED
  _quic conf `shouldBe` Nothing
#endif

parseConfig :: [String] -> IO Config
parseConfig args = withArgs args getConfig

shouldExitWith :: [String] -> ExitCode -> Expectation
shouldExitWith args expected = do
  result <- try (parseConfig args)
  case result of
    Left exitCode -> exitCode `shouldBe` expected
    Right _       -> expectationFailure "expected parser to exit"

longOptionArgs :: [String]
longOptionArgs =
  [ "--bind", "127.0.0.1"
  , "--port", "8080"
  , "--tls", "example.com:cert.pem:key.pem"
  , "--auth", "userpass.txt"
  , "--ws", "ws.example:443"
  , "--rev", "//example.com/api/upstream:80"
  , "--doh", "1.1.1.1:53"
  , "--hide"
  , "--naive"
  , "--name", "custom"
  , "--acme", "thumbprint"
  , "--log", "stderr"
  , "--loglevel", "debug"
#ifdef OS_UNIX
  , "--user", "nobody"
  , "--group", "nogroup"
#endif
#ifdef QUIC_ENABLED
  , "--quic", "8443"
#endif
  ]

shortOptionArgs :: [String]
shortOptionArgs =
  [ "-b", "0.0.0.0"
  , "-p", "8081"
  , "-s", "short.example:short-cert.pem:short-key.pem"
  , "-a", "short-userpass.txt"
#ifdef OS_UNIX
  , "-u", "daemon"
  , "-g", "daemon"
#endif
#ifdef QUIC_ENABLED
  , "-q", "9443"
#endif
  ]