packages feed

haskoin-core-1.5.0: test/Haskoin/UtilSpec.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | The helpers of "Haskoin.Util": bytes and hex, integers, lists and
-- templates, bit conversion, JSON options, the serialization helpers and
-- the marshalling classes.
module Haskoin.UtilSpec (spec) where

import Control.Exception (ErrorCall, evaluate)
import Control.Monad.Except (ExceptT, runExceptT)
import Data.Aeson
import Data.Aeson.Encoding (encodingToLazyByteString)
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.ByteString.Builder (toLazyByteString)
import Data.ByteString.Lazy qualified as BL
import Data.Bytes.Get
import Data.Bytes.Put
import Data.Default (def)
import Data.Either (isLeft)
import Data.Foldable (toList)
import Data.Int (Int32, Int64)
import Data.IntMap qualified as IntMap
import Data.List (permutations)
import Data.Maybe
import Data.Sequence qualified as Seq
import Data.Serialize qualified as S
import Data.Word (Word8)
import GHC.Generics (Generic)
import Haskoin.Crypto
import Haskoin.Util
import Haskoin.Util.Arbitrary
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck

identityTests :: IdentityTests
identityTests =
  def
    { readTests =
        [ ReadBox arbitraryNetwork
        ]
    }

-- A record and a sum type for the JSON options
data Point = Point {ptX :: Int, ptY :: Int}
  deriving (Eq, Show, Generic)

instance ToJSON Point where
  toJSON = genericToJSON (dropFieldLabel 2)

instance FromJSON Point where
  parseJSON = genericParseJSON (dropFieldLabel 2)

data Shape = SCircle {scRadius :: Int} | SSquare {ssSide :: Int}
  deriving (Eq, Show, Generic)

instance ToJSON Shape where
  toJSON = genericToJSON (dropSumLabels 1 2 "type")

instance FromJSON Shape where
  parseJSON = genericParseJSON (dropSumLabels 1 2 "type")

spec :: Spec
spec = prepareContext $ \ctx ->
  describe "utility functions" $ do
    testIdentity identityTests

    describe "Integers and bytes" $ do
      it "integerToBS writes big-endian bytes without leading zeros, and zero as one byte" $ do
        integerToBS 0 `shouldBe` "\x00"
        integerToBS 1 `shouldBe` "\x01"
        integerToBS 256 `shouldBe` "\x01\x00"
        integerToBS 0xdeadbeef `shouldBe` "\xde\xad\xbe\xef"
      it "integerToBS refuses a negative number" $
        evaluate (integerToBS (-1)) `shouldThrow` anyErrorCall
      it "bsToInteger reads big-endian bytes, and nothing as zero" $ do
        bsToInteger "\x01\x00" `shouldBe` 256
        bsToInteger "\x00\x00\x01" `shouldBe` 1
        bsToInteger "" `shouldBe` 0
      prop "bsToInteger . integerToBS" getPutInteger

    describe "Hex" $ do
      it "encodes lower case and decodes either case" $ do
        encodeHex "\xab\xcd" `shouldBe` "abcd"
        encodeHexLazy "\xab\xcd" `shouldBe` "abcd"
        decodeHex "abcd" `shouldBe` Just "\xab\xcd"
        decodeHex "ABCD" `shouldBe` Just "\xab\xcd"
        decodeHexLazy "ABCD" `shouldBe` Just "\xab\xcd"
        decodeHex "" `shouldBe` Just ""
      it "refuses an odd length, a character that is not hex, and spaces" $ do
        decodeHex "abc" `shouldBe` Nothing
        decodeHex "0g" `shouldBe` Nothing
        decodeHex " 00" `shouldBe` Nothing
        decodeHex "00 " `shouldBe` Nothing
        decodeHexLazy "abc" `shouldBe` Nothing
      it "hexEncoding is a JSON string of hex, and hexBuilder the hex alone" $ do
        encodingToLazyByteString (hexEncoding "\xab\xcd") `shouldBe` "\"abcd\""
        toLazyByteString (hexBuilder "\xab\xcd") `shouldBe` "abcd"
      prop "decodeHex . encodeHex" $ forAll arbitraryBS fromToHex
      prop "decodeHexLazy . encodeHexLazy" $ forAll arbitraryBS $ \bs -> decodeHexLazy (encodeHexLazy (BL.fromStrict bs)) == Just (BL.fromStrict bs)

    describe "getBits" $ do
      it "takes that many bits from the start, padded with zero bits" $ do
        getBits 4 "\xff\x00" `shouldBe` "\xf0"
        getBits 8 "\xff\x00" `shouldBe` "\xff"
        getBits 12 "\xff\xff" `shouldBe` "\xff\xf0"
        getBits 0 "\xff" `shouldBe` ""
      it "raises an error when the bytes do not hold that many bits" $ do
        evaluate (getBits 12 "\xff") `shouldThrow` anyErrorCall
        evaluate (getBits 4 "") `shouldThrow` anyErrorCall
        evaluate (getBits (-1) "\xff") `shouldThrow` anyErrorCall

    describe "Maybe and Either" $ do
      prop "eitherToMaybe" testEitherToMaybe
      prop "maybeToEither" testMaybeToEither
      it "liftMaybe and liftEither lift into ExceptT" $ do
        runExceptT (liftMaybe "none" (Just (1 :: Int))) `shouldReturn` Right 1
        runExceptT (liftMaybe "none" (Nothing :: Maybe Int)) `shouldReturn` Left "none"
        runExceptT (liftEither (Right (2 :: Int)) :: ExceptT' Int) `shouldReturn` Right 2
        runExceptT (liftEither (Left "bad") :: ExceptT' Int) `shouldReturn` Left "bad"

    describe "updateIndex" $ do
      it "changes the element at the index, and nothing when the index is out of range" $ do
        updateIndex 1 [1, 2, 3 :: Int] (* 10) `shouldBe` [1, 20, 3]
        updateIndex 0 [1, 2, 3 :: Int] (* 10) `shouldBe` [10, 2, 3]
        updateIndex 3 [1, 2, 3 :: Int] (* 10) `shouldBe` [1, 2, 3]
        updateIndex (-1) [1, 2, 3 :: Int] (* 10) `shouldBe` [1, 2, 3]
      prop "compare updateIndex with Data.Sequence" testUpdateIndex

    describe "matchTemplate" $ do
      it "gives for each element of the template the first matching input, each input once" $ do
        matchTemplate [1, 2, 2 :: Int] [2, 2, 3] (==) `shouldBe` [Just 2, Just 2, Nothing]
        matchTemplate [2 :: Int] [2, 2] (==) `shouldBe` [Just 2, Nothing]
        matchTemplate [] [1, 2 :: Int] (==) `shouldBe` [Nothing, Nothing]
        matchTemplate [1 :: Int] [] (==) `shouldBe` []
        matchTemplate ["a", "bb" :: String] [2, 1 :: Int] (\s n -> length s == n) `shouldBe` [Just "bb", Just "a"]
      prop "matchTemplate" testMatchTemplate
      prop "testing matchTemplate with two lists" testMatchTemplateLen

    describe "Triples" $
      it "fst3, snd3 and lst3 take the parts of a triple" $ do
        fst3 (1 :: Int, "b", 'c') `shouldBe` 1
        snd3 (1 :: Int, "b", 'c') `shouldBe` "b"
        lst3 (1 :: Int, "b", 'c') `shouldBe` 'c'

    describe "JSON options" $ do
      it "dropFieldLabel drops the prefix of a field and lowers its case" $ do
        encode (Point 1 2) `shouldBe` "{\"x\":1,\"y\":2}"
        decode "{\"x\":1,\"y\":2}" `shouldBe` Just (Point 1 2)
      it "dropSumLabels also drops the prefix of a constructor, which goes in the tag field" $ do
        encode (SCircle 3) `shouldBe` "{\"radius\":3,\"type\":\"circle\"}"
        encode (SSquare 4) `shouldBe` "{\"side\":4,\"type\":\"square\"}"
        decode "{\"type\":\"square\",\"side\":4}" `shouldBe` Just (SSquare 4)

    describe "convertBits" $ do
      -- 0xff is 11111111: the group 11111 and 111 padded to 11100
      it "regroups bits, padding the last group with zeros when asked" $ do
        convertBits True 8 5 [0xff] `shouldBe` Just [31, 28]
        convertBits True 5 8 [1] `shouldBe` Just [8]
        convertBits True 8 5 [] `shouldBe` Just []
      it "without padding, refuses leftover bits that are not zero or that could hold a whole digit" $ do
        convertBits False 5 8 [31, 28] `shouldBe` Just [255]
        convertBits False 5 8 [31, 31] `shouldBe` Nothing
        convertBits False 5 8 [1, 0] `shouldBe` Just [8]
        convertBits False 5 8 [1] `shouldBe` Nothing
        convertBits False 5 8 [] `shouldBe` Just []
      -- The data part of the BIP 173 address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4, as 5-bit groups, is the key hash of its example
      it "converts the 5-bit groups of the BIP 173 example to its 20-byte program" $
        fmap (B.pack . map fromIntegral) (convertBits False 5 8 [14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, 8, 21, 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22])
          `shouldBe` Just "\x75\x1e\x76\xe8\x19\x91\x96\xd4\x54\x94\x1c\x45\xd1\xb3\xa3\x23\xf1\x43\x3b\xd6"
      prop "converting to 5 bits with padding and back gives the bytes" $
        forAll arbitraryBS $ \bs ->
          let groups = fromJust (convertBits True 8 5 (map fromIntegral (B.unpack bs)))
           in fmap (map fromIntegral) (convertBits False 5 8 groups) === Just (B.unpack bs)

    describe "Serialization helpers" $ do
      it "putList and getList write a 64-bit count then the elements" $ do
        runPutS (putList putWord8 [1, 2]) `shouldBe` "\x00\x00\x00\x00\x00\x00\x00\x02\x01\x02"
        runGetS (getList getWord8) "\x00\x00\x00\x00\x00\x00\x00\x02\x01\x02" `shouldBe` Right [1, 2]
        runGetS (getList getWord8) "\x00\x00\x00\x00\x00\x00\x00\x00" `shouldBe` Right ([] :: [Word8])
      it "getList refuses a count that does not fit an Int instead of reading an empty list" $
        runGetS (getList getWord8) "\xff\xff\xff\xff\xff\xff\xff\xff\x41" `shouldSatisfy` isLeft
      it "putMaybe and getMaybe write a tag byte" $ do
        runPutS (putMaybe putWord8 Nothing) `shouldBe` "\x00"
        runPutS (putMaybe putWord8 (Just 7)) `shouldBe` "\x01\x07"
        runGetS (getMaybe getWord8) "\x01\x07" `shouldBe` Right (Just 7)
        runGetS (getMaybe getWord8) "\x00" `shouldBe` Right (Nothing :: Maybe Word8)
        runGetS (getMaybe getWord8) "\x02" `shouldSatisfy` isLeft
      it "putLengthBytes and getLengthBytes write a 64-bit length then the bytes" $ do
        runPutS (putLengthBytes "abc") `shouldBe` "\x00\x00\x00\x00\x00\x00\x00\x03\&abc"
        runGetS getLengthBytes "\x00\x00\x00\x00\x00\x00\x00\x03\&abc" `shouldBe` Right "abc"
        runGetS getLengthBytes "\xff\xff\xff\xff\xff\xff\xff\xff\x41" `shouldSatisfy` isLeft
        runGetS getLengthBytes "\x00\x00\x00\x00\x00\x00\x00\x03\&ab" `shouldSatisfy` isLeft
      it "putInteger writes a 32-bit number after a zero byte, and a wider one with its sign, its length and its bytes" $ do
        runPutS (putInteger 0) `shouldBe` "\x00\x00\x00\x00\x00"
        runPutS (putInteger (-1)) `shouldBe` "\x00\xff\xff\xff\xff"
        runPutS (putInteger 2147483647) `shouldBe` "\x00\x7f\xff\xff\xff"
        -- 2^31: sign 1, four bytes, little-endian
        runPutS (putInteger 2147483648) `shouldBe` "\x01\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x80"
        -- -(2^31 + 1): sign 0xff
        runPutS (putInteger (-2147483649)) `shouldBe` "\x01\xff\x00\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x80"
      it "getInteger reads both forms back" $ do
        runGetS getInteger "\x00\xff\xff\xff\xff" `shouldBe` Right (-1)
        runGetS getInteger "\x01\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x80" `shouldBe` Right 2147483648
        runGetS getInteger "\x01\xff\x00\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x80" `shouldBe` Right (-2147483649)
      prop "getInteger . putInteger" $ \i -> runGetS getInteger (runPutS (putInteger i)) === Right (i :: Integer)
      prop "getInteger . putInteger for wide numbers" $ \(i :: Integer) (k :: Int) ->
        let n = i * 2 ^ (abs k `mod` 256) in runGetS getInteger (runPutS (putInteger n)) === Right n
      it "putInt32be and putInt64be write two's complement, big-endian" $ do
        runPutS (putInt32be (-1)) `shouldBe` "\xff\xff\xff\xff"
        runPutS (putInt32be minBound) `shouldBe` "\x80\x00\x00\x00"
        runPutS (putInt32be 1) `shouldBe` "\x00\x00\x00\x01"
        runPutS (putInt64be (-2)) `shouldBe` "\xff\xff\xff\xff\xff\xff\xff\xfe"
        runPutS (putInt64be minBound) `shouldBe` "\x80\x00\x00\x00\x00\x00\x00\x00"
        runGetS getInt32be "\x80\x00\x00\x00" `shouldBe` Right (minBound :: Int32)
        runGetS getInt64be "\xff\xff\xff\xff\xff\xff\xff\xfe" `shouldBe` Right (-2 :: Int64)
      prop "getInt32be . putInt32be" $ \i -> runGetS getInt32be (runPutS (putInt32be i)) === Right (i :: Int32)
      prop "getInt64be . putInt64be" $ \i -> runGetS getInt64be (runPutS (putInt64be i)) === Right (i :: Int64)
      it "putIntMap and getIntMap write the pairs in key order after their count" $ do
        let m = IntMap.fromList [(2, 20), (1, 10 :: Word8)]
        runPutS (putIntMap (putWord8 . fromIntegral) putWord8 m) `shouldBe` "\x00\x00\x00\x00\x00\x00\x00\x02\x01\x0a\x02\x14"
        runGetS (getIntMap (fromIntegral <$> getWord8) getWord8) "\x00\x00\x00\x00\x00\x00\x00\x02\x01\x0a\x02\x14" `shouldBe` Right m
      it "putTwo and getTwo write a pair" $ do
        runPutS (putTwo putWord8 putWord16be (1, 2)) `shouldBe` "\x01\x00\x02"
        runGetS (getTwo getWord8 getWord16be) "\x01\x00\x02" `shouldBe` Right (1, 2)

    describe "Marshalling" $ do
      let key = fromJust (secKey (B.replicate 32 1))
          public = PublicKey (derivePubKey ctx key) True
      it "marshal and unmarshal round trip, strict and lazy" $ do
        unmarshal ctx (marshal ctx public) `shouldBe` Right public
        unmarshalLazy ctx (marshalLazy ctx public) `shouldBe` Right public
        (unmarshalLazy ctx "junk" :: Either String PublicKey) `shouldSatisfy` isLeft
      it "marshalJSON and unmarshalJSON round trip, and refuse what is not the value" $ do
        unmarshalJSON ctx (marshalJSON ctx public) `shouldBe` Just public
        (unmarshalJSON ctx "\"junk\"" :: Maybe PublicKey) `shouldBe` Nothing
        (unmarshalJSON ctx "{" :: Maybe PublicKey) `shouldBe` Nothing

    describe "Test helpers" $ do
      it "customCerealID tells whether a getter reads what a putter writes" $ do
        customCerealID S.getWord8 S.putWord8 7 `shouldBe` True
        customCerealID S.getWord16be S.putWord16le 7 `shouldBe` False
      it "readTestFile reads a JSON file of the data directory" $ do
        value <- readTestFile "sig_strict.json" :: IO Value
        value `shouldSatisfy` \v -> case v of Array _ -> True; _ -> False
        parsed <- readTestFileParser parseJSON "sig_strict.json" :: IO [Value]
        length parsed `shouldSatisfy` (> 0)
      it "readTestFile raises an error for a file that is not there" $
        (readTestFile "no-such-file.json" :: IO Value) `shouldThrow` anyException

type ExceptT' a = ExceptTString a

type ExceptTString a = ExceptT String IO a

getPutInteger :: Integer -> Bool
getPutInteger i = bsToInteger (integerToBS $ abs i) == abs i

fromToHex :: ByteString -> Bool
fromToHex bs = decodeHex (encodeHex bs) == Just bs

testUpdateIndex :: [Int] -> Int -> Int -> Bool
testUpdateIndex xs v i =
  updateIndex i xs (const v) == toList (Seq.update i v $ Seq.fromList xs)

testMatchTemplate :: [Int] -> Int -> Bool
testMatchTemplate as i = catMaybes res == bs
  where
    res = matchTemplate as bs (==)
    idx =
      if null as
        then 0
        else i `mod` length as
    bs = permutations as !! idx

testMatchTemplateLen :: [Int] -> [Int] -> Bool
testMatchTemplateLen as bs = length bs == length res
  where
    res = matchTemplate as bs (==)

testEitherToMaybe :: Either String Int -> Bool
testEitherToMaybe (Right v) = eitherToMaybe (Right v) == Just v
testEitherToMaybe e = isNothing (eitherToMaybe e)

testMaybeToEither :: Maybe Int -> String -> Bool
testMaybeToEither (Just v) str = maybeToEither str (Just v) == Right v
testMaybeToEither m str = maybeToEither str m == Left str