Files
simplex-chat/tests/BadgeTests.hs
T
EvgenyandEvgeny @ SimpleX Chat 735662902f core, ui: make badge expiration non-optional (#7445)
* core, ui: make badge expiration non-optional

* update

* update bot types

* fix to use non-optional badge expiry

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-09-02 08:14:09 +01:00

137 lines
5.6 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module BadgeTests (badgeTests) where
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime, nominalDay)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import qualified Data.Aeson as J
import qualified Simplex.Messaging.Crypto as C
import Simplex.Chat.Badges
import Simplex.Messaging.Crypto.BBS
import Test.Hspec
badgeTests :: Spec
badgeTests = do
it "full workflow: request, issue, verify credential, generate and verify proof" testFullWorkflow
it "should reject badge with tampered type" testTamperedType
it "should reject badge with tampered expiry" testTamperedExpiry
it "should reject badge with wrong server key" testWrongKey
it "should report a key index missing from configured keys" testUnknownKeyIdx
it "should compute badge status correctly" testExpiryCheck
it "should accept unknown badge types" testUnknownBadgeType
it "credential serializes to a paste-able token and back" testCredentialSerialization
proofOf :: BadgeProof -> BBSProof
proofOf (BadgeProof _ _ p _) = p
testKeyIdx :: Int
testKeyIdx = 1
keysFor :: BBSPublicKey -> Map Int BBSPublicKey
keysFor = M.singleton testKeyIdx
testFullWorkflow :: IO ()
testFullWorkflow = do
Right (pk, sk) <- bbsKeyGen
drg <- C.newRandom
mk <- generateMasterKey drg
let req = BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = BTSupporter, badgeExpiry = futureTime, badgeExtra = ""}}
Just vreq <- verifyPayment (BPRedeemCode "TEST") req
Right cred <- issueBadge testKeyIdx sk vreq
let BadgeCredential idx mk' _ _ = cred
idx `shouldBe` testKeyIdx
mk' `shouldBe` mk
verifyCredential pk cred >>= (`shouldBe` True)
Right badge <- generateBadgeProof pk cred (BBSPresHeader "nonce-1")
-- the proof inherits the credential's key index, so receivers find the right key
let BadgeProof {badgeKeyIdx} = badge
badgeKeyIdx `shouldBe` testKeyIdx
verifyBadge (keysFor pk) badge >>= (`shouldBe` Just True)
Right badge2 <- generateBadgeProof pk cred (BBSPresHeader "nonce-2")
verifyBadge (keysFor pk) badge2 >>= (`shouldBe` Just True)
proofOf badge `shouldNotBe` proofOf badge2
testTamperedType :: IO ()
testTamperedType = do
(pk, BadgeProof idx ph p info) <- issueBadgeProof BTSupporter futureTime
verifyBadge (keysFor pk) (BadgeProof idx ph p info {badgeType = BTLegend}) >>= (`shouldBe` Just False)
testTamperedExpiry :: IO ()
testTamperedExpiry = do
(pk, BadgeProof idx ph p info) <- issueBadgeProof BTSupporter futureTime
verifyBadge (keysFor pk) (BadgeProof idx ph p info {badgeExpiry = pastTime}) >>= (`shouldBe` Just False)
testWrongKey :: IO ()
testWrongKey = do
(_, badge) <- issueBadgeProof BTSupporter futureTime
Right (pk2, _) <- bbsKeyGen
verifyBadge (keysFor pk2) badge >>= (`shouldBe` Just False)
testUnknownKeyIdx :: IO ()
testUnknownKeyIdx = do
(pk, badge) <- issueBadgeProof BTSupporter futureTime
-- a key index not in the configured keys cannot be verified at all (Nothing)
verifyBadge (M.singleton (testKeyIdx + 1) pk) badge >>= (`shouldBe` Nothing)
testExpiryCheck :: IO ()
testExpiryCheck = do
now <- getCurrentTime
let info expiry = BadgeInfo {badgeType = BTSupporter, badgeExpiry = expiry, badgeExtra = ""}
futureInfo = info futureTime
expiredAgo d = info $ addUTCTime (- (d * nominalDay)) now
mkBadgeStatus now (Just True) futureInfo `shouldBe` BSActive
-- the badge stays active for a week after its expiry
mkBadgeStatus now (Just True) (expiredAgo 1) `shouldBe` BSActive
mkBadgeStatus now (Just True) (expiredAgo 6) `shouldBe` BSActive
-- then it is shown as expired for 31 days
mkBadgeStatus now (Just True) (expiredAgo 8) `shouldBe` BSExpired
mkBadgeStatus now (Just True) (expiredAgo 37) `shouldBe` BSExpired
mkBadgeStatus now (Just True) (expiredAgo 39) `shouldBe` BSExpiredOld
mkBadgeStatus now (Just True) (info pastTime) `shouldBe` BSExpiredOld
mkBadgeStatus now (Just False) futureInfo `shouldBe` BSFailed
mkBadgeStatus now Nothing futureInfo `shouldBe` BSUnknownKey
testUnknownBadgeType :: IO ()
testUnknownBadgeType = do
(pk, badge) <- issueBadgeProof (BTUnknown "future_type") futureTime
verifyBadge (keysFor pk) badge >>= (`shouldBe` Just True)
testCredentialSerialization :: IO ()
testCredentialSerialization = do
Right (pk, sk) <- bbsKeyGen
drg <- C.newRandom
mk <- generateMasterKey drg
let mkCred expiry = do
Right cred <- issueBadge testKeyIdx sk (VerifiedBadgeRequest BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = BTSupporter, badgeExpiry = expiry, badgeExtra = ""}})
pure cred
dated <- mkCred futureTime
J.eitherDecode (J.encode dated) `shouldBe` Right dated
-- a decoded credential still verifies against the issuing key
case J.eitherDecode (J.encode dated) of
Right cred -> verifyCredential pk cred >>= (`shouldBe` True)
Left e -> expectationFailure e
-- Helpers
futureTime :: UTCTime
futureTime = posixSecondsToUTCTime 4102444800 -- 2099-12-31
pastTime :: UTCTime
pastTime = posixSecondsToUTCTime 1577836800 -- 2020-01-01
issueBadgeProof :: BadgeType -> UTCTime -> IO (BBSPublicKey, BadgeProof)
issueBadgeProof bt expiry = do
Right (pk, sk) <- bbsKeyGen
drg <- C.newRandom
mk <- generateMasterKey drg
let vreq = VerifiedBadgeRequest BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = bt, badgeExpiry = expiry, badgeExtra = ""}}
Right cred <- issueBadge testKeyIdx sk vreq
Right badge <- generateBadgeProof pk cred (BBSPresHeader "test-nonce")
pure (pk, badge)