packages feed

gibberish-3.0.0.0: test/Data/Gibberish/Gen/PassSpec.hs

module Data.Gibberish.Gen.PassSpec (spec) where

import Data.Gibberish.Gen.Pass
import Data.Gibberish.Gen.Trigraph (loadTrigraph)
import Data.Gibberish.Monad.Pass (usingPass, usingPassT)
import Data.Gibberish.Types
import Data.Gibberish.Utils (numeralConversions, symbolConversions)
import Test.Gibberish.Gen qualified as Gen

import Control.Monad.IO.Class (liftIO)
import Data.Char
import Data.Map qualified as Map
import Data.Text qualified as Text
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Test.Hspec
import Test.Hspec.Hedgehog
import Prelude hiding (Word)

spec :: Spec
spec = do
  trigraphs <-
    sequence
      [ runIO (loadTrigraph English),
        runIO (loadTrigraph Spanish)
      ]
  let genTrigraph = Gen.element trigraphs

  describe "genPassword" $ do
    it "has correct length" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen

      let opts' = opts {woptsTrigraph = trigraph}

      let (Word pass, _) = usingPass randomGen (genPassword opts')
      annotateShow pass

      Text.length pass === woptsLength opts

    it "has only lowercase when capitals is false" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen
      -- Only consider passwords of sufficient (>=3) length
      len <- forAll (Gen.int $ Range.linear 3 15)

      let opts' =
            opts
              { woptsTrigraph = trigraph,
                woptsCapitals = False,
                woptsLength = len
              }

      let (Word pass, _) = usingPass randomGen (genPassword opts')
      annotateShow pass

      assert $ Text.all (not . isUpper) pass

    it "has at least one capital when enabled" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen
      -- Only consider passwords of sufficient (>=3) length
      len <- forAll (Gen.int $ Range.linear 3 15)

      let opts' =
            opts
              { woptsTrigraph = trigraph,
                woptsCapitals = True,
                woptsLength = len
              }

      let (Word pass, _) = usingPass randomGen (genPassword opts')
      annotateShow pass

      assert $ Text.any (\c -> isUpper c || isPunctuation c) pass

    it "sometimes has multiple capitals when enabled" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen
      -- Only consider passwords of sufficient (>=10) length
      len <- forAll (Gen.int $ Range.linear 10 20)

      let opts' =
            opts
              { woptsTrigraph = trigraph,
                woptsCapitals = True,
                woptsLength = len
              }

      let (Word pass, _) = usingPass randomGen (genPassword opts')
      annotateShow pass

      cover 10 "has multiple capitals" $
        Text.length (Text.filter isUpper pass) > 1

    it "has at least one digit when enabled" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen
      -- Only consider passwords of sufficient (>=3) length
      len <- forAll (Gen.int $ Range.linear 3 15)

      let opts' =
            opts
              { woptsTrigraph = trigraph,
                woptsDigits = True,
                woptsLength = len
              }

      let (Word pass, _) = usingPass randomGen (genPassword opts')
      annotateShow pass

      assert $
        Text.any isNumber pass
          || Text.all (`Map.notMember` numeralConversions) pass

    it "sometimes has multiple digits when enabled" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen
      -- Only consider passwords of sufficient (>=10) length
      len <- forAll (Gen.int $ Range.linear 10 20)

      let opts' =
            opts
              { woptsTrigraph = trigraph,
                woptsDigits = True,
                woptsLength = len
              }

      let (Word pass, _) = usingPass randomGen (genPassword opts')
      annotateShow pass

      cover 10 "has multiple digits" $
        Text.length (Text.filter isNumber pass) > 1

    it "usually has at least one special when enabled" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen
      -- Only consider passwords of sufficient (>=3) length
      len <- forAll (Gen.int $ Range.linear 3 15)

      let opts' =
            opts
              { woptsTrigraph = trigraph,
                woptsSpecials = True,
                woptsLength = len
              }

      let (Word pass, _) = usingPass randomGen (genPassword opts')
      annotateShow pass

      let allSymbols = concat (Map.elems symbolConversions)

      cover 50 "has at least one special" $
        Text.any (`elem` allSymbols) pass

    it "sometimes has at multiple specials when enabled" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen
      -- Only consider passwords of sufficient (>=10) length
      len <- forAll (Gen.int $ Range.linear 10 20)

      let opts' =
            opts
              { woptsTrigraph = trigraph,
                woptsSpecials = True,
                woptsLength = len
              }

      let (Word pass, _) = usingPass randomGen (genPassword opts')
      annotateShow pass

      let allSymbols = concat (Map.elems symbolConversions)

      cover 10 "has at least one special" $
        Text.length (Text.filter (`elem` allSymbols) pass) > 1

  describe "genPasswords" $ do
    it "passwords have the correct length" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      randomGen <- forAll Gen.stdGen

      let opts' = opts {woptsTrigraph = trigraph}

      let (passes, _) = usingPass randomGen (genPasswords opts')
      annotateShow passes

      listSize <- forAll $ Gen.int (Range.linear 1 50)

      assert $
        not (null passes)
          && all (`shouldHaveLength` woptsLength opts') (take listSize passes)

  describe "genPasswords'" $ do
    it "passwords have the correct length" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      numberWords <- forAll $ Gen.int (Range.linear 1 100)
      randomGen <- forAll Gen.stdGen

      let opts' = opts {woptsTrigraph = trigraph}

      (passes, _) <- liftIO $ usingPassT randomGen (genPasswords' opts' numberWords)
      annotateShow passes

      assert $
        not (null passes)
          && all (`shouldHaveLength` woptsLength opts') passes

    it "has the correct number of passwords" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPasswordOpts
      numberWords <- forAll $ Gen.int (Range.linear 1 100)
      randomGen <- forAll Gen.stdGen

      let opts' = opts {woptsTrigraph = trigraph}

      (passes, _) <- liftIO $ usingPassT randomGen (genPasswords' opts' numberWords)
      annotateShow passes

      length passes === numberWords

  describe "genPassphrase" $ do
    it "words have correct length" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPassphraseOpts
      randomGen <- forAll Gen.stdGen

      let opts' = opts {poptsTrigraph = trigraph}

      let (phrase, _) = usingPass randomGen (genPassphrase opts')
      annotateShow phrase

      let minLen = poptsMinLength opts'
          maxLen = poptsMaxLength opts'
          isInRange w = Text.length w >= minLen && Text.length w <= maxLen

      listSize <- forAll $ Gen.int (Range.linear 1 25)

      assert $
        not (null phrase)
          && all (isInRange . unWord) (take listSize phrase)

  describe "genPassphrase'" $ do
    it "has the correct number of words" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPassphraseOpts
      numberWords <- forAll $ Gen.int (Range.linear 0 100)
      randomGen <- forAll Gen.stdGen

      let opts' = opts {poptsTrigraph = trigraph}

      (phrase, _) <- usingPassT randomGen (genPassphrase' opts' numberWords)
      annotateShow phrase

      length phrase === numberWords

    it "words have correct length" $ hedgehog $ do
      trigraph <- forAll genTrigraph
      opts <- forAll Gen.genPassphraseOpts
      numberWords <- forAll $ Gen.int (Range.linear 1 100)
      randomGen <- forAll Gen.stdGen

      let opts' = opts {poptsTrigraph = trigraph}

      (phrase, _) <- liftIO $ usingPassT randomGen (genPassphrase' opts' numberWords)
      annotateShow phrase

      let minLen = poptsMinLength opts'
          maxLen = poptsMaxLength opts'
          isInRange w = Text.length w >= minLen && Text.length w <= maxLen

      assert $
        not (null phrase) && all (isInRange . unWord) phrase

shouldHaveLength :: Word -> Int -> Bool
shouldHaveLength (Word w) = (Text.length w ==)