packages feed

sd-jwt-0.1.0.0: test/DisclosureSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module DisclosureSpec (spec) where

import Test.Hspec
import Test.QuickCheck
import Test.QuickCheck.Property ((==>))
import TestHelpers
import TestKeys
import SDJWT.Internal.Types
import SDJWT.Internal.Utils
import SDJWT.Internal.Digest
import SDJWT.Internal.Disclosure
import SDJWT.Internal.Serialization
import SDJWT.Internal.Issuance
import SDJWT.Internal.Presentation
import SDJWT.Internal.Verification (verifySDJWT, verifySDJWTSignature, verifySDJWTWithoutSignature, verifyKeyBinding, verifyDisclosures, extractHashAlgorithm)
import SDJWT.Internal.KeyBinding
import SDJWT.Internal.JWT
import qualified Data.Vector as V
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8, decodeUtf8')
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Map.Strict as Map
import Data.Int (Int64)
import Data.Maybe (isJust, mapMaybe)
import Data.List (find, nub)
import Control.Monad (replicateM)

spec :: Spec
spec = describe "SDJWT.Disclosure" $ do
  describe "createObjectDisclosure" $ do
    it "creates disclosure with string value" $ do
      salt <- generateSalt
      let name = "given_name"
      let value = Aeson.String "John"
      case createObjectDisclosure (Salt salt) name value of
        Right _ -> return ()  -- Success
        Left err -> expectationFailure $ "Failed to create disclosure: " ++ show err
    
    it "creates disclosure with empty string value" $ do
      salt <- generateSalt
      let name = "empty_claim"
      let value = Aeson.String ""
      case createObjectDisclosure (Salt salt) name value of
        Right _ -> return ()  -- Success
        Left err -> expectationFailure $ "Failed to create disclosure: " ++ show err
    
    it "creates disclosure with numeric value" $ do
      salt <- generateSalt
      let name = "age"
      let value = Aeson.Number 42
      case createObjectDisclosure (Salt salt) name value of
        Right _ -> return ()  -- Success
        Left err -> expectationFailure $ "Failed to create disclosure: " ++ show err
    
    it "creates disclosure with object value" $ do
        salt <- generateSalt
        let name = "address"
        let value = Aeson.Object $ KeyMap.fromList [ (Key.fromText "street", Aeson.String "123 Main St")]
        case createObjectDisclosure (Salt salt) name value of
          Right _ -> return ()  -- Success
          Left err -> expectationFailure $ "Failed to create disclosure: " ++ show err
    
    describe "createArrayDisclosure" $ do
      it "creates a valid array disclosure" $ do
        salt <- generateSalt
        let value = Aeson.String "US"
        case createArrayDisclosure (Salt salt) value of
          Right _ -> return ()  -- Success
          Left err -> expectationFailure $ "Failed to create disclosure: " ++ show err
      
      it "creates array disclosure with object value" $ do
        salt <- generateSalt
        let value = Aeson.Object $ KeyMap.fromList [ (Key.fromText "name", Aeson.String "John")]
        case createArrayDisclosure (Salt salt) value of
          Right _ -> return ()  -- Success
          Left err -> expectationFailure $ "Failed to create disclosure: " ++ show err
    
    describe "encodeDisclosure" $ do
      it "encodes object disclosure" $ do
        salt <- generateSalt
        let name = "test_claim"
        let value = Aeson.String "test_value"
        case createObjectDisclosure (Salt salt) name value of
          Right encoded1 -> do
            case decodeDisclosure encoded1 of
              Right decoded -> do
                -- Round-trip: encode -> decode -> encode should match
                let encoded2 = encodeDisclosure decoded
                unEncodedDisclosure encoded1 `shouldBe` unEncodedDisclosure encoded2
              Left err -> expectationFailure $ "Failed to decode: " ++ show err
          Left err -> expectationFailure $ "Failed to create: " ++ show err
      
      it "encodes array disclosure" $ do
        salt <- generateSalt
        let value = Aeson.String "array_value"
        case createArrayDisclosure (Salt salt) value of
          Right encoded1 -> do
            case decodeDisclosure encoded1 of
              Right decoded -> do
                -- Round-trip: encode -> decode -> encode should match
                let encoded2 = encodeDisclosure decoded
                unEncodedDisclosure encoded1 `shouldBe` unEncodedDisclosure encoded2
              Left err -> expectationFailure $ "Failed to decode: " ++ show err
          Left err -> expectationFailure $ "Failed to create: " ++ show err
    
    describe "getDisclosureSalt" $ do
      it "extracts salt from object disclosure" $ do
        salt <- generateSalt
        let name = "test_claim"
        let value = Aeson.String "test_value"
        case createObjectDisclosure (Salt salt) name value of
          Right encoded -> do
            case decodeDisclosure encoded of
              Right disclosure -> do
                unSalt (getDisclosureSalt disclosure) `shouldBe` salt
              Left err -> expectationFailure $ "Failed to decode: " ++ show err
          Left err -> expectationFailure $ "Failed to create: " ++ show err
      
      it "extracts salt from array disclosure" $ do
        salt <- generateSalt
        let value = Aeson.String "array_value"
        case createArrayDisclosure (Salt salt) value of
          Right encoded -> do
            case decodeDisclosure encoded of
              Right disclosure -> do
                unSalt (getDisclosureSalt disclosure) `shouldBe` salt
              Left err -> expectationFailure $ "Failed to decode: " ++ show err
          Left err -> expectationFailure $ "Failed to create: " ++ show err
    
    describe "decodeDisclosure" $ do
      it "decodes RFC example disclosure" $ do
        -- From RFC 9901 Section 5.1: given_name disclosure
        let encoded = EncodedDisclosure "WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd"
        case decodeDisclosure encoded of
          Right disclosure -> do
            getDisclosureClaimName disclosure `shouldBe` Just "given_name"
            getDisclosureValue disclosure `shouldBe` Aeson.String "John"
          Left err -> expectationFailure $ "Failed to decode: " ++ show err
      
      it "decodes array disclosure (2 elements)" $ do
        -- Array disclosure: [salt, value]
        salt <- generateSalt
        let value = Aeson.String "US"
        case createArrayDisclosure (Salt salt) value of
          Right encoded -> do
            case decodeDisclosure encoded of
              Right disclosure -> do
                getDisclosureClaimName disclosure `shouldBe` Nothing  -- Array disclosures have no name
                getDisclosureValue disclosure `shouldBe` value
              Left err -> expectationFailure $ "Failed to decode: " ++ show err
          Left err -> expectationFailure $ "Failed to create: " ++ show err
      
      it "fails to decode invalid base64url" $ do
        let invalid = EncodedDisclosure "not-valid-base64url!!!"
        case decodeDisclosure invalid of
          Left (InvalidDisclosureFormat _) -> return ()  -- Expected error
          Left _ -> return ()  -- Any error is acceptable
          Right _ -> expectationFailure "Should fail to decode invalid base64url"
      
      it "fails to decode non-array JSON" $ do
        -- Base64url-encoded JSON object instead of array
        let jsonObj = KeyMap.fromList [ (Key.fromText "key", Aeson.String "value")]
        let jsonBytes = BS.concat $ BSL.toChunks $ Aeson.encode jsonObj
        let encoded = EncodedDisclosure $ base64urlEncode jsonBytes
        case decodeDisclosure encoded of
          Left (InvalidDisclosureFormat _) -> return ()  -- Expected error
          Left _ -> return ()  -- Any error is acceptable
          Right _ -> expectationFailure "Should fail to decode non-array JSON"
      
      it "fails to decode array with 1 element" $ do
        -- Array with only 1 element (should have 2 or 3)
        let jsonArray = Aeson.Array $ V.fromList [Aeson.String "salt"]
        let jsonBytes = BS.concat $ BSL.toChunks $ Aeson.encode jsonArray
        let encoded = EncodedDisclosure $ base64urlEncode jsonBytes
        case decodeDisclosure encoded of
          Left (InvalidDisclosureFormat msg) -> do
            T.isInfixOf "must have 2 or 3 elements" msg `shouldBe` True
          Left _ -> return ()  -- Any error is acceptable
          Right _ -> expectationFailure "Should fail to decode array with 1 element"
      
      it "fails to decode array with 4+ elements" $ do
        -- Array with 4 elements (should have 2 or 3)
        let jsonArray = Aeson.Array $ V.fromList
              [ Aeson.String "salt"
              , Aeson.String "name"
              , Aeson.String "value"
              , Aeson.String "extra"
              ]
        let jsonBytes = BS.concat $ BSL.toChunks $ Aeson.encode jsonArray
        let encoded = EncodedDisclosure $ base64urlEncode jsonBytes
        case decodeDisclosure encoded of
          Left (InvalidDisclosureFormat msg) -> do
            T.isInfixOf "must have 2 or 3 elements" msg `shouldBe` True
          Left _ -> return ()  -- Any error is acceptable
          Right _ -> expectationFailure "Should fail to decode array with 4+ elements"
      
      it "fails to decode array where first element is not a string" $ do
        -- First element (salt) must be a string
        let jsonArray = Aeson.Array $ V.fromList
              [ Aeson.Number 123  -- Not a string
              , Aeson.String "name"
              , Aeson.String "value"
              ]
        let jsonBytes = BS.concat $ BSL.toChunks $ Aeson.encode jsonArray
        let encoded = EncodedDisclosure $ base64urlEncode jsonBytes
        case decodeDisclosure encoded of
          Left (InvalidDisclosureFormat _) -> return ()  -- Expected error
          Left _ -> return ()  -- Any error is acceptable
          Right _ -> expectationFailure "Should fail when salt is not a string"
      
      it "fails to decode array disclosure where second element is not a string" $ do
        -- For object disclosure (3 elements), second element (name) must be a string
        -- But for array disclosure (2 elements), second element is the value (can be anything)
        -- So this test is for object disclosure
        let jsonArray = Aeson.Array $ V.fromList
              [ Aeson.String "salt"
              , Aeson.Number 123  -- Name should be string
              , Aeson.String "value"
              ]
        let jsonBytes = BS.concat $ BSL.toChunks $ Aeson.encode jsonArray
        let encoded = EncodedDisclosure $ base64urlEncode jsonBytes
        case decodeDisclosure encoded of
          Left (InvalidDisclosureFormat _) -> return ()  -- Expected error
          Left _ -> return ()  -- Any error is acceptable
          Right _ -> expectationFailure "Should fail when claim name is not a string"