packages feed

sd-jwt-0.1.0.0: test/PropertySpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-orphans -Wno-name-shadowing #-}
module PropertySpec (spec) where

import Test.Hspec
import Test.QuickCheck
import Test.QuickCheck.Property ((==>))
import TestHelpers
import SDJWT.Internal.Types
import SDJWT.Internal.Utils
import SDJWT.Internal.Digest
import SDJWT.Internal.Disclosure
import SDJWT.Internal.Serialization
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8')
import qualified Data.ByteString as BS
import Control.Monad (replicateM)
import Data.List (nub)

-- QuickCheck Arbitrary instances for property-based testing

instance Arbitrary HashAlgorithm where
  arbitrary = elements [SHA256, SHA384, SHA512]

instance Arbitrary Salt where
  arbitrary = do
    bytes <- vectorOf 16 arbitrary  -- RFC 9901 requires 128 bits (16 bytes)
    return $ Salt (BS.pack bytes)

instance Arbitrary BS.ByteString where
  arbitrary = BS.pack <$> listOf arbitrary

instance Arbitrary T.Text where
  arbitrary = T.pack <$> listOf (choose ('a', 'z'))

spec :: Spec
spec = describe "Property-Based Tests" $ do
  describe "Base64url Encoding/Decoding" $ do
    it "round-trips correctly for any ByteString" $ property $ \bs ->
      case base64urlDecode (base64urlEncode bs) of
        Right decoded -> decoded == bs
        Left _ -> False
  
  describe "Text/ByteString Conversions" $ do
    it "textToByteString and byteStringToText round-trip" $ property $ \txt ->
      byteStringToText (textToByteString txt) == txt
    
    it "byteStringToText and textToByteString round-trip (valid UTF-8)" $ property $ \bs ->
      -- Only test with valid UTF-8 ByteStrings
      case decodeUtf8' bs of
        Right _ -> textToByteString (byteStringToText bs) == bs
        Left _ -> True  -- Discard invalid UTF-8
  
  describe "Salt Generation" $ do
    it "generates 16-byte salts" $ do
        salt <- generateSalt
        BS.length salt `shouldBe` 16
    
    it "generates unique salts (100 samples)" $ do
      salts <- replicateM 100 generateSalt
      let uniqueSalts = nub salts
      length uniqueSalts `shouldBe` length salts
  
  describe "Hash Algorithm" $ do
    it "hashAlgorithmToText and parseHashAlgorithm round-trip" $ property $ \alg ->
      parseHashAlgorithm (hashAlgorithmToText alg) == Just alg
    
    it "parseHashAlgorithm rejects invalid strings" $ property $ \txt ->
      txt `notElem` ["sha-256", "sha-384", "sha-512"] ==>
        parseHashAlgorithm txt == Nothing
  
  describe "Disclosure Encoding/Decoding" $ do
    it "object disclosure round-trips" $ property $ \salt name value ->
      case createObjectDisclosure (Salt salt) name value of
        Right encoded -> case decodeDisclosure encoded of
          Right (DisclosureObject (ObjectDisclosure decodedSalt decodedName decodedValue)) ->
            decodedSalt == Salt salt && decodedName == name && decodedValue == value
          _ -> False
        Left _ -> True  -- Discard if creation fails
    
    it "array disclosure round-trips" $ property $ \salt value ->
      case createArrayDisclosure (Salt salt) value of
        Right encoded -> case decodeDisclosure encoded of
          Right (DisclosureArray (ArrayDisclosure decodedSalt decodedValue)) ->
            decodedSalt == Salt salt && decodedValue == value
          _ -> False
        Left _ -> True  -- Discard if creation fails
    
    it "encodeDisclosure and decodeDisclosure round-trip for object disclosures" $ property $ \salt name value ->
      case createObjectDisclosure (Salt salt) name value of
        Right encoded1 -> case decodeDisclosure encoded1 of
          Right disclosure -> encodeDisclosure disclosure == encoded1
          Left _ -> False
        Left _ -> True  -- Discard if creation fails
    
    it "encodeDisclosure and decodeDisclosure round-trip for array disclosures" $ property $ \salt value ->
      case createArrayDisclosure (Salt salt) value of
        Right encoded1 -> case decodeDisclosure encoded1 of
          Right disclosure -> encodeDisclosure disclosure == encoded1
          Left _ -> False
        Left _ -> True  -- Discard if creation fails
  
  describe "Digest Computation" $ do
    it "same disclosure produces same digest" $ property $ \alg salt name value ->
      case createObjectDisclosure (Salt salt) name value of
        Right encoded ->
          let digest1 = computeDigest alg encoded
              digest2 = computeDigest alg encoded
          in digest1 == digest2
        Left _ -> True  -- Discard if creation fails
    
    it "different salts produce different digests" $ property $ \alg salt1 salt2 name value ->
      salt1 /= salt2 ==>
        case (createObjectDisclosure (Salt salt1) name value, createObjectDisclosure (Salt salt2) name value) of
          (Right enc1, Right enc2) -> computeDigest alg enc1 /= computeDigest alg enc2
          _ -> True  -- Discard if creation fails
    
    it "different values produce different digests" $ property $ \alg salt name value1 value2 ->
      value1 /= value2 ==>
        case (createObjectDisclosure (Salt salt) name value1, createObjectDisclosure (Salt salt) name value2) of
          (Right enc1, Right enc2) -> computeDigest alg enc1 /= computeDigest alg enc2
          _ -> True  -- Discard if creation fails
    
    it "verifyDigest succeeds for correct digest" $ property $ \alg salt name value ->
        case createObjectDisclosure (Salt salt) name value of
        Right encoded ->
          let digest = computeDigest alg encoded
          in verifyDigest alg digest encoded
        Left _ -> True  -- Discard if creation fails
    
    it "verifyDigest fails for incorrect digest" $ property $ \alg salt name value ->
        case createObjectDisclosure (Salt salt) name value of
        Right encoded ->
          let correctDigest = computeDigest alg encoded
              wrongDigest = Digest (T.reverse (unDigest correctDigest))
          in verifyDigest alg wrongDigest encoded == False
        Left _ -> True  -- Discard if creation fails
  
  describe "Serialization" $ do
    it "serializeSDJWT and deserializeSDJWT round-trip (no KB)" $ property $ \jwt disclosures ->
      let sdjwt = SDJWT jwt (map EncodedDisclosure disclosures)
          serialized = serializeSDJWT sdjwt
      in case deserializeSDJWT serialized of
        Right (SDJWT decodedJWT decodedDisclosures) ->
          decodedJWT == jwt && decodedDisclosures == map EncodedDisclosure disclosures
        Left _ -> False
    
    it "serializePresentation and deserializePresentation round-trip (no KB)" $ property $ \jwt disclosures ->
      let presentation = SDJWTPresentation jwt (map EncodedDisclosure disclosures) Nothing
          serialized = serializePresentation presentation
      in case deserializePresentation serialized of
        Right (SDJWTPresentation decodedJWT decodedDisclosures decodedKB) ->
          decodedJWT == jwt && decodedDisclosures == map EncodedDisclosure disclosures && decodedKB == Nothing
        Left _ -> False
    
    it "serializePresentation and deserializePresentation round-trip (with KB)" $ property $ \jwt disclosures kbJWT ->
      -- Only test with non-empty JWTs (empty strings aren't valid JWTs)
      jwt /= "" && kbJWT /= "" ==>
        let presentation = SDJWTPresentation jwt (map EncodedDisclosure disclosures) (Just kbJWT)
            serialized = serializePresentation presentation
        in case deserializePresentation serialized of
          Right (SDJWTPresentation decodedJWT decodedDisclosures decodedKB) ->
            decodedJWT == jwt && decodedDisclosures == map EncodedDisclosure disclosures && decodedKB == Just kbJWT
          Left _ -> False
  
  describe "Hash Algorithm Properties" $ do
    it "hashAlgorithmToText never returns empty string" $ property $ \alg ->
      hashAlgorithmToText alg /= ""
    
    it "parseHashAlgorithm round-trips with hashAlgorithmToText" $ property $ \alg ->
      parseHashAlgorithm (hashAlgorithmToText alg) == Just alg
    
    it "defaultHashAlgorithm returns SHA256" $
      defaultHashAlgorithm `shouldBe` SHA256
  
  describe "Disclosure Properties" $ do
    it "getDisclosureSalt extracts correct salt from object disclosure" $ property $ \salt name value ->
        case createObjectDisclosure (Salt salt) name value of
        Right encoded -> case decodeDisclosure encoded of
          Right disclosure -> getDisclosureSalt disclosure == Salt salt
          Left _ -> False
        Left _ -> True  -- Discard if creation fails
    
    it "getDisclosureSalt extracts correct salt from array disclosure" $ property $ \salt value ->
        case createArrayDisclosure (Salt salt) value of
        Right encoded -> case decodeDisclosure encoded of
          Right disclosure -> getDisclosureSalt disclosure == Salt salt
          Left _ -> False
        Left _ -> True  -- Discard if creation fails
    
    it "getDisclosureClaimName returns Nothing for array disclosures" $ property $ \salt value ->
        case createArrayDisclosure (Salt salt) value of
        Right encoded -> case decodeDisclosure encoded of
          Right disclosure -> getDisclosureClaimName disclosure == Nothing
          Left _ -> False
        Left _ -> True  -- Discard if creation fails
    
    it "getDisclosureClaimName returns Just name for object disclosures" $ property $ \salt name value ->
      case createObjectDisclosure (Salt salt) name value of
        Right encoded -> case decodeDisclosure encoded of
          Right disclosure -> getDisclosureClaimName disclosure == Just name
          Left _ -> False
        Left _ -> True  -- Discard if creation fails