packages feed

hegel-0.1.0: test/Hegel/ProtocolSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Hegel.ProtocolSpec (spec) where

import Codec.CBOR.Term (Term (..))
import qualified Data.ByteString as BS
import Data.Bits (shiftL, shiftR, (.|.))
import Data.Word (Word32)
import System.IO (Handle, hFlush, hSetBinaryMode)
import System.Process (createPipe)
import Test.Hspec

import Hegel.Protocol

-- | Helper: create a pipe pair of handles for testing.
createPipePair :: IO (Handle, Handle)
createPipePair = createPipe

-- | Pack a Word32 as 4 big-endian bytes (reimplemented for testing since
-- the library does not export this).
testPackWord32BE :: Word32 -> BS.ByteString
testPackWord32BE w = BS.pack
  [ fromIntegral (w `shiftR` 24)
  , fromIntegral (w `shiftR` 16)
  , fromIntegral (w `shiftR` 8)
  , fromIntegral w
  ]

-- | Unpack a Word32 from 4 big-endian bytes at offset 0.
testUnpackWord32BE :: BS.ByteString -> Word32
testUnpackWord32BE bs =
  let b0 = fromIntegral (BS.index bs 0) :: Word32
      b1 = fromIntegral (BS.index bs 1) :: Word32
      b2 = fromIntegral (BS.index bs 2) :: Word32
      b3 = fromIntegral (BS.index bs 3) :: Word32
  in (b0 `shiftL` 24) .|. (b1 `shiftL` 16) .|. (b2 `shiftL` 8) .|. b3

spec :: Spec
spec = do
  describe "CRC32" $ do
    it "computes the known check value for '123456789'" $ do
      let input = "123456789"
      computeCRC32 input `shouldBe` 0xCBF43926

  describe "pack/unpack Word32 BE" $ do
    it "round-trips uint32 values" $ do
      let values = [0, 1, 255, 256, 65535, 0x12345678, 0xFFFFFFFF] :: [Word32]
      mapM_
        ( \w -> do
            let packed = testPackWord32BE w
            BS.length packed `shouldBe` 4
            testUnpackWord32BE packed `shouldBe` w
        )
        values

  describe "Packet round-trip" $ do
    it "writes and reads a packet through a pipe pair" $ do
      (readH, writeH) <- createPipePair
      hSetBinaryMode readH True
      hSetBinaryMode writeH True
      let pkt =
            Packet
              { packetChannelId = 42,
                packetMessageId = 7,
                packetIsReply = False,
                packetPayload = encodeTerm (TString "hello")
              }
      writePacket writeH pkt
      result <- readPacket readH
      packetChannelId result `shouldBe` 42
      packetMessageId result `shouldBe` 7
      packetIsReply result `shouldBe` False
      decodeTerm (packetPayload result) `shouldBe` TString "hello"

    it "round-trips a reply packet" $ do
      (readH, writeH) <- createPipePair
      hSetBinaryMode readH True
      hSetBinaryMode writeH True
      let pkt =
            Packet
              { packetChannelId = 10,
                packetMessageId = 3,
                packetIsReply = True,
                packetPayload = encodeTerm (TBool True)
              }
      writePacket writeH pkt
      result <- readPacket readH
      packetChannelId result `shouldBe` 10
      packetMessageId result `shouldBe` 3
      packetIsReply result `shouldBe` True

  describe "Invalid magic" $ do
    it "raises on invalid magic number" $ do
      (readH, writeH) <- createPipePair
      hSetBinaryMode readH True
      hSetBinaryMode writeH True
      -- Write a header with bad magic (0xDEADBEEF), followed by terminator
      let badHeader =
            BS.concat
              [ testPackWord32BE 0xDEADBEEF,
                testPackWord32BE 0,
                testPackWord32BE 1,
                testPackWord32BE 1,
                testPackWord32BE 0
              ]
      BS.hPut writeH badHeader
      BS.hPut writeH (BS.singleton 0x0A)
      hFlush writeH
      readPacket readH `shouldThrow` anyIOException

  describe "Bad CRC" $ do
    it "raises on checksum mismatch" $ do
      (readH, writeH) <- createPipePair
      hSetBinaryMode readH True
      hSetBinaryMode writeH True
      let payload = encodeTerm (TString "test")
      let payloadLen = fromIntegral (BS.length payload) :: Word32
      -- Compute correct CRC then corrupt it
      let headerForCRC =
            BS.concat
              [ testPackWord32BE magic,
                testPackWord32BE 0,
                testPackWord32BE 1,
                testPackWord32BE 1,
                testPackWord32BE payloadLen
              ]
      let correctCRC = computeCRC32Parts headerForCRC payload
      let wrongCRC = correctCRC + 1
      let badHeader =
            BS.concat
              [ testPackWord32BE magic,
                testPackWord32BE wrongCRC,
                testPackWord32BE 1,
                testPackWord32BE 1,
                testPackWord32BE payloadLen
              ]
      BS.hPut writeH badHeader
      BS.hPut writeH payload
      BS.hPut writeH (BS.singleton 0x0A)
      hFlush writeH
      readPacket readH `shouldThrow` anyIOException