@@ -0,0 +1,238 @@
1+{-# LANGUAGE DerivingStrategies #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+import Control.Applicative
5+import Control.Monad
6+import qualified Data.ByteString as BS
7+import qualified Data.ByteString.Char8 as BSC
8+import Data.Either
9+import Data.PhoneNumber.Number
10+import Data.PhoneNumber.Util
11+import qualified Data.Set as S
12+import Test.Hspec
13+import Test.Hspec.QuickCheck
14+import Test.QuickCheck
15+
16+newtype CC = CC CountryCode
17+ deriving newtype (Show)
18+
19+instance Arbitrary CC where
20+ arbitrary = CC . CountryCode <$> choose (-10, 1000)
21+ shrink (CC (CountryCode cc)) = CC . CountryCode <$> shrink cc
22+
23+newtype Reg = Reg Region
24+ deriving newtype (Show)
25+
26+instance Arbitrary Reg where
27+ arbitrary = do
28+ c1 <- elements ['A'..'Z']
29+ c2 <- elements ['A'..'Z']
30+ pure $ Reg $ Region $ BSC.pack [c1, c2]
31+
32+newtype MReg = MReg (Maybe Region)
33+ deriving newtype (Show)
34+
35+instance Arbitrary MReg where
36+ arbitrary = MReg . fmap (\(Reg r) -> r) <$> arbitrary
37+ shrink (MReg (Just _)) = [MReg Nothing]
38+ shrink (MReg Nothing) = []
39+
40+newtype ParserInput = ParserInput BS.ByteString
41+ deriving newtype (Show)
42+
43+instance Arbitrary ParserInput where
44+ arbitrary = do
45+ len <- choose (0, 20)
46+ xs <- replicateM len $ frequency
47+ [ (10, elements "#()+-0123456789")
48+ , (1, elements "'*,./:;") ]
49+ pure $ ParserInput $ BSC.pack xs
50+ shrink (ParserInput bs) = ParserInput . BS.pack <$> shrink (BS.unpack bs)
51+
52+data ParsedNumber = ParsedNumber (ParseMode, Maybe Region, ParserInput) PhoneNumber
53+ deriving stock (Show)
54+
55+instance Arbitrary ParsedNumber where
56+ arbitrary = liftA3 (,,) (elements [Canonicalize, KeepRawInput]) arbitrary arbitrary
57+ `suchThatMap` \(mode, MReg mRegion, ParserInput input) ->
58+ case parseNumber mode mRegion input of
59+ Right pn -> Just $ ParsedNumber (mode, mRegion, ParserInput input) pn
60+ _ -> Nothing
61+ shrink (ParsedNumber (mode, region, input) _) = do
62+ ParserInput input' <- shrink input
63+ case parseNumber mode region input' of
64+ Right pn -> pure $ ParsedNumber (mode, region, ParserInput input') pn
65+ _ -> empty
66+
67+regZZ, reg001 :: Region
68+regZZ = "ZZ"
69+reg001 = "001"
70+
71+cc0 :: CountryCode
72+cc0 = 0
73+
74+main :: IO ()
75+main = hspec $ modifyMaxSuccess (* 10) $ do
76+ describe "supportedRegions" $ do
77+ prop "doesn't contain ZZ" $ regZZ `S.notMember` supportedRegions
78+ prop "doesn't contain 001" $ reg001 `S.notMember` supportedRegions
79+
80+ describe "supportedGlobalNetworkCallingCodes" $ do
81+ prop "doesn't contain 0" $ cc0 `S.notMember` supportedGlobalNetworkCallingCodes
82+ prop "is a subset of supportedCallingCodes" $
83+ supportedGlobalNetworkCallingCodes `S.isSubsetOf` supportedCallingCodes
84+
85+ describe "supportedCallingCodes" $ do
86+ prop "doesn't contain 0" $ cc0 `S.notMember` supportedCallingCodes
87+
88+ describe "supportedTypesForRegion" $ do
89+ prop "doesn't contain FixedLineOrMobile" $ \(Reg region) ->
90+ FixedLineOrMobile `S.notMember` supportedTypesForRegion region
91+
92+ describe "supportedTypesForNonGeoEntity" $ do
93+ prop "doesn't contain FixedLineOrMobile" $ \(CC cc) ->
94+ FixedLineOrMobile `S.notMember` supportedTypesForNonGeoEntity cc
95+
96+ describe "convertAlphaNumber" $ do
97+ prop "doesn't change the parsing of a number" $ \(MReg mRegion) (ParserInput str) ->
98+ parseNumber Canonicalize mRegion (convertAlphaNumber str)
99+ === parseNumber Canonicalize mRegion str
100+ prop "returns a number that is not alpha" $ \(ParserInput str) ->
101+ not $ isAlphaNumber $ convertAlphaNumber str
102+
103+ describe "normalizeNumber" $ do
104+ prop "returns ASCII digits unmodified" $
105+ forAll (elements [Digits, Dialable]) $ \mode ->
106+ forAll (listOf $ elements ['0'..'9']) $ \str ->
107+ BSC.pack str == normalizeNumber mode (BSC.pack str)
108+ describe "Digits" $ do
109+ prop "returns only ASCII digits" $ \str ->
110+ BSC.all (`elem` ['0'..'9']) $ normalizeNumber Digits $ BSC.pack str
111+ prop "doesn't randomly fail" $ \str ->
112+ not (BS.null $ normalizeNumber Digits $ "0" <> BS.pack str)
113+ .&&. not (BS.null $ normalizeNumber Digits $ BS.pack str <> "0")
114+ describe "Dialable" $ do
115+ -- PhoneNumberUtil::NormalizeDiallableCharsOnly will error on any invalid UTF8
116+ xprop "doesn't randomly fail" $ \str ->
117+ not (BS.null $ normalizeNumber Digits $ "0" <> BS.pack str)
118+ .&&. not (BS.null $ normalizeNumber Digits $ BS.pack str <> "0")
119+
120+ describe "nationalSignificantNumber" $ do
121+ prop "is non-empty" $ \(ParsedNumber _ pn) ->
122+ not $ BS.null $ nationalSignificantNumber pn
123+ prop "returns only ASCII digits" $ \(ParsedNumber _ pn) ->
124+ BSC.all (`elem` ['0'..'9']) $ nationalSignificantNumber pn
125+ modifyMaxDiscardRatio (* 10) $ do
126+ prop "concatenated with country code parses to the original (if valid and not MX)" $
127+ \(ParsedNumber (_, _, ParserInput input) pn) ->
128+ -- Apparently "+52112210000000" makes the parser/formatter do weird
129+ -- stuff as it tries to handle the deprecated (?) mobile token ("1"
130+ -- after "+52")
131+ if "5211" `BS.isPrefixOf` normalizeNumber Digits input
132+ then discard
133+ else isValidNumber Nothing pn ==>
134+ let
135+ cc = countryCode pn
136+ nsn = nationalSignificantNumber pn
137+ str = "+" <> BSC.pack (show cc) <> nsn
138+ in case parseNumber Canonicalize Nothing str of
139+ Left err -> error $ show err
140+ Right pn' -> cc === countryCode pn'
141+ .&&. nsn === nationalSignificantNumber pn'
142+
143+ describe "countryMobileToken" $ do
144+ prop "returns only ASCII digits" $ \(CC cc) ->
145+ BSC.all (`elem` ['0'..'9']) $ countryMobileToken cc
146+
147+ describe "formatNumber" $ do
148+ prop "with International produces a parseable result" $
149+ \(ParsedNumber _ pn) -> possibleNumber Unknown pn == IsPossible ==>
150+ isRight $ parseNumber Canonicalize Nothing
151+ $ formatNumber International pn
152+ prop "with E164 produces a parseable result" $
153+ \(ParsedNumber _ pn) -> possibleNumber Unknown pn == IsPossible ==>
154+ isRight $ parseNumber Canonicalize Nothing
155+ $ formatNumber E164 pn
156+ prop "with RFC3966 produces a parseable result" $
157+ \(ParsedNumber _ pn) -> possibleNumber Unknown pn == IsPossible ==>
158+ isRight $ parseNumber Canonicalize Nothing
159+ $ formatNumber RFC3966 pn
160+
161+ describe "truncateTooLongNumber" $ do
162+ pure ()
163+
164+ describe "numberType" $ do
165+ pure ()
166+
167+ describe "isValidNumber" $ do
168+ modifyMaxDiscardRatio (* 10) $ do
169+ prop "is preserved under parse . format" $ \(ParsedNumber _ pn) ->
170+ isValidNumber Nothing pn ==>
171+ case parseNumber Canonicalize Nothing $ formatNumber E164 pn of
172+ Left err -> error $ show err
173+ Right pn' -> isValidNumber Nothing pn'
174+
175+ describe "regionForNumber" $ do
176+ prop "doesn't return ZZ" $ \(ParsedNumber _ pn) ->
177+ regionForNumber pn =/= Just (Right regZZ)
178+ prop "doesn't return 001" $ \(ParsedNumber _ pn) ->
179+ regionForNumber pn =/= Just (Right reg001)
180+ prop "returns a supported region" $ \(ParsedNumber _ pn) ->
181+ case regionForNumber pn of
182+ Just (Right region) -> region `S.member` supportedRegions
183+ _ -> discard
184+ prop "returns Region001 exactly for supported non-geo entities" $
185+ \(ParsedNumber _ pn) ->
186+ (countryCode pn `S.member` supportedGlobalNetworkCallingCodes)
187+ === (regionForNumber pn == Just (Left Region001))
188+
189+ describe "countryCodeForRegion" $ do
190+ prop "doesn't return 0" $ \(Reg region) ->
191+ countryCodeForRegion region =/= Just cc0
192+ prop "returns codes in supportedCallingCodes" $ \(Reg region) ->
193+ case countryCodeForRegion region of
194+ Nothing -> discard
195+ Just cc -> cc `S.member` supportedCallingCodes
196+
197+ describe "regionForCountryCode" $ do
198+ prop "doesn't return ZZ" $ \(CC cc) ->
199+ regionForCountryCode cc =/= Just (Right regZZ)
200+ prop "doesn't return 001" $ \(CC cc) ->
201+ regionForCountryCode cc =/= Just (Right reg001)
202+ prop "returns Region001 exactly for supported non-geo entities" $ \(CC cc) ->
203+ (cc `S.member` supportedGlobalNetworkCallingCodes)
204+ === (regionForCountryCode cc == Just (Left Region001))
205+
206+ describe "regionsForCountryCode" $ do
207+ prop "doesn't return ZZ" $ \(CC cc) ->
208+ Right regZZ `notElem` regionsForCountryCode cc
209+ prop "doesn't return 001" $ \(CC cc) ->
210+ Right reg001 `notElem` regionsForCountryCode cc
211+
212+ describe "isNANPACountry" $ do
213+ pure ()
214+
215+ describe "nddPrefixForRegion" $ do
216+ prop "with True returns only ASCII digits" $ \(Reg region) ->
217+ BSC.all (`elem` ['0'..'9']) $ nddPrefixForRegion True region
218+
219+ describe "possibleNumber" $ do
220+ modifyMaxDiscardRatio (* 10) $ do
221+ prop "with Unknown is more lenient than isValidNumber" $
222+ \(ParsedNumber _ pn) ->
223+ isValidNumber Nothing pn ==> possibleNumber Unknown pn === IsPossible
224+
225+ describe "canBeInternationallyDialed" $ do
226+ pure ()
227+
228+ describe "isGeographicalNumber" $ do
229+ prop "matches isGeographicalNumberType" $ \(ParsedNumber _ pn) ->
230+ isGeographicalNumber pn
231+ === isGeographicalNumberType (numberType pn) (countryCode pn)
232+
233+ describe "isGeographicalNumberType" $ do
234+ pure ()
235+
236+ describe "parseNumber" $ do
237+ prop "doesn't return a number with CountryCode 0" $ \(ParsedNumber _ pn) ->
238+ countryCode pn /= cc0