taiwan-id-0.1.1.0: components/test/taiwan-id-test/Main.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{- HLINT ignore "Redundant bracket" -}
module Main (main) where
import Lens.Micro
( Lens', lens, set )
import Lens.Micro.Extras
( view )
import Taiwan.ID
( ID (..) )
import Taiwan.ID.CharIndex
( CharIndex (CharIndex) )
import Taiwan.ID.CharSet
( CharSet (..) )
import Taiwan.ID.Digit
( Digit (..) )
import Taiwan.ID.Digit1289
( Digit1289 (..) )
import Taiwan.ID.Gender
( Gender (..) )
import Taiwan.ID.Issuer
( Issuer (..) )
import Taiwan.ID.Language
( Language (..) )
import Taiwan.ID.Letter
( Letter (..) )
import Taiwan.ID.Region
( Region )
import Taiwan.ID.Test
( unsafePokeChar )
import Test.Hspec
( Spec, describe, hspec, it, shouldBe, shouldSatisfy )
import Test.QuickCheck
( Arbitrary (..)
, NonEmptyList (..)
, Property
, choose
, elements
, forAll
, property
, (===)
)
import Test.QuickCheck.Classes
( boundedEnumLaws, eqLaws, numLaws, ordLaws, showLaws, showReadLaws )
import Test.QuickCheck.Classes.Hspec
( testLawsMany )
import qualified Data.Finitary as Finitary
import qualified Data.Set.NonEmpty as NESet
import qualified Data.Text as T
import qualified Taiwan.ID as ID
import qualified Taiwan.ID.Test as Test
instance Arbitrary Digit where
arbitrary = Test.genDigit
shrink = Test.shrinkDigit
instance Arbitrary Digit1289 where
arbitrary = Test.genDigit1289
shrink = Test.shrinkDigit1289
instance Arbitrary Gender where
arbitrary = Test.genGender
shrink = Test.shrinkGender
instance Arbitrary ID where
arbitrary = Test.genID
shrink = Test.shrinkID
instance Arbitrary Issuer where
arbitrary = Test.genIssuer
shrink = Test.shrinkIssuer
instance Arbitrary Language where
arbitrary = Test.genLanguage
shrink = Test.shrinkLanguage
instance Arbitrary Letter where
arbitrary = Test.genLetter
shrink = Test.shrinkLetter
instance Arbitrary Region where
arbitrary = Test.genRegion
shrink = Test.shrinkRegion
main :: IO ()
main = hspec $ do
describe "Class laws" $ do
testLawsMany @Digit
[ boundedEnumLaws
, eqLaws
, numLaws
, ordLaws
, showLaws
, showReadLaws
]
testLawsMany @Digit1289
[ boundedEnumLaws
, eqLaws
, ordLaws
, showLaws
, showReadLaws
]
testLawsMany @Gender
[ boundedEnumLaws
, eqLaws
, ordLaws
, showLaws
, showReadLaws
]
testLawsMany @ID
[ eqLaws
, ordLaws
, showLaws
, showReadLaws
]
testLawsMany @Issuer
[ boundedEnumLaws
, eqLaws
, ordLaws
, showLaws
, showReadLaws
]
testLawsMany @Language
[ boundedEnumLaws
, eqLaws
, ordLaws
, showLaws
, showReadLaws
]
testLawsMany @Letter
[ boundedEnumLaws
, eqLaws
, ordLaws
, showLaws
, showReadLaws
]
testLawsMany @Region
[ boundedEnumLaws
, eqLaws
, ordLaws
, showLaws
, showReadLaws
]
describe "Finitary instances" $ do
describe "ID" $ do
let start = Finitary.start @ID
end = Finitary.end @ID
previous = Finitary.previous @ID
next = Finitary.next @ID
it "previous start" $
previous start
`shouldBe` Nothing
it "start" $
start
`shouldBe` ID.fromSymbol @"A100000001"
it "next start" $
next start
`shouldBe` Just (ID.fromSymbol @"A100000010")
it "previous end" $
previous end
`shouldBe` Just (ID.fromSymbol @"Z999999987")
it "end" $
end
`shouldBe` ID.fromSymbol @"Z999999996"
it "next end" $
next end
`shouldBe` Nothing
describe "ID attribute getters and setters" $ do
describe "Gender" $
checkLensLaws gender
describe "Issuer" $
checkLensLaws issuer
describe "Region" $
checkLensLaws region
describe "ID.fromText" $ do
it "successfully parses known-valid identification numbers" $
forAll (elements knownValidIDs) $ \i ->
ID.fromText (ID.toText i) `shouldBe` Right i
it "successfully parses valid identification numbers" $
property $
forAll Test.genID $ \validID ->
ID.fromText (ID.toText validID) `shouldBe` Right validID
it "does not parse identification numbers that are too short" $
property $
forAll Test.genIDTextInvalidLengthTooShort $ \invalidID ->
ID.fromText invalidID `shouldBe` Left ID.InvalidLength
it "does not parse identification numbers that are too long" $
property $
forAll Test.genIDTextInvalidLengthTooLong $ \invalidID ->
ID.fromText invalidID `shouldBe` Left ID.InvalidLength
it "does not parse identification numbers with invalid region codes" $
property $
forAll (Test.genIDTextInvalidCharAtIndex 0) $ \invalidID ->
ID.fromText invalidID `shouldBe` Left
(ID.InvalidChar 0 (CharRange 'A' 'Z'))
it "does not parse identification numbers with invalid initial digits" $
property $
forAll (Test.genIDTextInvalidCharAtIndex 1) $ \invalidID ->
ID.fromText invalidID `shouldBe` Left
(ID.InvalidChar 1 (CharSet $ NESet.fromList ['1', '2', '8', '9']))
it "does not parse identification numbers with invalid serial digits" $
property $
forAll (choose (2, 9)) $ \invalidCharIndex ->
forAll (Test.genIDTextInvalidCharAtIndex invalidCharIndex) $ \invalidID ->
ID.fromText invalidID `shouldBe` Left
(ID.InvalidChar (CharIndex invalidCharIndex) (CharRange '0' '9'))
it "does not parse identification numbers with invalid checksums" $
property $
forAll Test.genIDTextInvalidChecksum $ \invalidID ->
ID.fromText invalidID `shouldBe` Left ID.InvalidChecksum
it "reports invalid characters even when input is too short" $
property $ \(i :: ID) ->
forAll Test.genInvalidChar $ \invalidChar ->
forAll (choose (1, 9)) $ \truncatedLength ->
forAll (choose (0, truncatedLength - 1)) $ \invalidCharIndex -> do
let truncatedID = T.take truncatedLength (ID.toText i)
let invalidID = unsafePokeChar invalidCharIndex truncatedID invalidChar
ID.fromText invalidID `shouldSatisfy` \case
Left (ID.InvalidChar (CharIndex index) _)
| index == invalidCharIndex -> True
_ -> False
it "does not report invalid characters if input is too long" $
property $ \(NonEmpty trailingExcess) ->
forAll Test.genIDTextInvalidChar $ \invalidID ->
ID.fromText (invalidID <> T.pack trailingExcess) `shouldBe`
Left ID.InvalidLength
checkLensLaws
:: forall i v. (Arbitrary i, Arbitrary v, Eq i, Eq v, Show i, Show v)
=> Lens' i v
-> Spec
checkLensLaws l =
do
it "Finality"
$ property lensLawFinality
it "Idempotence"
$ property lensLawIdempotence
it "Invertibility"
$ property lensLawInvertibility
it "Reversibility"
$ property lensLawReversibility
it "Stability"
$ property lensLawStability
where
lensLawFinality :: i -> v -> v -> Property
lensLawFinality i v1 v2 = set l v2 (set l v1 i) === set l v2 i
lensLawIdempotence :: i -> v -> Property
lensLawIdempotence i v = set l v (set l v i) === set l v i
lensLawInvertibility :: i -> v -> Property
lensLawInvertibility i v = view l (set l v i) === v
lensLawReversibility :: i -> v -> Property
lensLawReversibility i v = set l (view l i) (set l v i) === i
lensLawStability :: i -> Property
lensLawStability i = set l (view l i) i === i
gender :: Lens' ID Gender
gender = lens ID.getGender (flip ID.setGender)
issuer :: Lens' ID Issuer
issuer = lens ID.getIssuer (flip ID.setIssuer)
region :: Lens' ID Region
region = lens ID.getRegion (flip ID.setRegion)
-- | A set of known-valid ID numbers.
--
-- Generated with 身分證字號產生器.
--
-- See: https://www.csie.ntu.edu.tw/~b90057/use/ROCid.html
--
knownValidIDs :: [ID]
knownValidIDs =
[ ID.fromSymbol @"A123961383"
, ID.fromSymbol @"B210742224"
, ID.fromSymbol @"C120930548"
, ID.fromSymbol @"D257991149"
, ID.fromSymbol @"E127379116"
, ID.fromSymbol @"F235628112"
, ID.fromSymbol @"G105851924"
, ID.fromSymbol @"H247910878"
, ID.fromSymbol @"I118949082"
, ID.fromSymbol @"J218475156"
, ID.fromSymbol @"K150252170"
, ID.fromSymbol @"L298479266"
, ID.fromSymbol @"M114415878"
, ID.fromSymbol @"N242846162"
, ID.fromSymbol @"O184333688"
, ID.fromSymbol @"P257366789"
, ID.fromSymbol @"Q163999855"
, ID.fromSymbol @"R275744925"
, ID.fromSymbol @"S158047168"
, ID.fromSymbol @"T296696104"
, ID.fromSymbol @"U108929984"
, ID.fromSymbol @"V245356279"
, ID.fromSymbol @"W127612989"
, ID.fromSymbol @"X234128072"
, ID.fromSymbol @"Y140531128"
, ID.fromSymbol @"Z250358466"
]