packages feed

idn-0.1.2.0: test/Data/Text/IDNSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Data.Text.IDNSpec (spec) where

import Test.Hspec
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.IDN

spec :: Spec
spec = do
  describe "toASCII" $ do
    it "converts simple ASCII domains unchanged" $ do
      toASCII "example.com" `shouldBe` Right "example.com"
    
    it "converts German Unicode domain to Punycode" $ do
      case toASCII "münchen.de" of
        Right result -> result `shouldSatisfy` T.isPrefixOf "xn--"
        Left err -> expectationFailure $ "Failed: " ++ show err
    
    it "converts Chinese domain to Punycode" $ do
      case toASCII "中国.com" of
        Right result -> T.isInfixOf "xn--" result `shouldBe` True
        Left err -> expectationFailure $ "Failed: " ++ show err
    
    it "converts Arabic domain to Punycode" $ do
      case toASCII "مصر.com" of
        Right result -> T.isInfixOf "xn--" result `shouldBe` True
        Left err -> expectationFailure $ "Failed: " ++ show err
    
    it "converts Russian domain to Punycode" $ do
      case toASCII "испытание.рф" of
        Right result -> T.isInfixOf "xn--" result `shouldBe` True
        Left err -> expectationFailure $ "Failed: " ++ show err
    
    it "converts Japanese domain to Punycode" $ do
      case toASCII "日本.jp" of
        Right result -> T.isInfixOf "xn--" result `shouldBe` True
        Left err -> expectationFailure $ "Failed: " ++ show err
    
    it "handles empty input" $ do
      toASCII "" `shouldSatisfy` isLeft
    
    it "rejects labels that are too long (>63 octets)" $ do
      let longLabel = T.replicate 64 "a"
      toASCII longLabel `shouldSatisfy` isLeft
    
    it "rejects domains longer than 253 octets" $ do
      let longDomain = T.intercalate "." (replicate 50 "label")
      toASCII longDomain `shouldSatisfy` isLeft
    
    it "rejects invalid hyphen positions" $ do
      toASCII "-example.com" `shouldSatisfy` isLeft
      toASCII "example-.com" `shouldSatisfy` isLeft
      toASCII "ab--cd.com" `shouldSatisfy` isLeft
    
    it "rejects xn-- prefix with pure ASCII (RFC 5891 violation)" $ do
      -- RFC 5891: Punycode should only be used for labels with non-ASCII characters
      toASCII "xn--test.com" `shouldSatisfy` isLeft
    
    it "handles multi-label domains" $ do
      toASCII "sub.example.com" `shouldBe` Right "sub.example.com"
    
    it "handles mixed ASCII and Unicode labels" $ do
      case toASCII "sub.münchen.de" of
        Right result -> do
          T.isInfixOf "sub" result `shouldBe` True
          T.isInfixOf "xn--" result `shouldBe` True
        Left err -> expectationFailure $ "Failed: " ++ show err
  
  describe "toUnicode" $ do
    it "converts simple ASCII domains unchanged" $ do
      toUnicode "example.com" `shouldBe` Right "example.com"
    
    it "decodes German Punycode A-label" $ do
      case toUnicode "xn--mnchen-3ya.de" of
        Right result -> T.isInfixOf "ü" result `shouldBe` True
        Left err -> expectationFailure $ "Failed: " ++ show err
    
    it "handles empty input" $ do
      toUnicode "" `shouldSatisfy` isLeft
    
    it "handles multi-label domains" $ do
      toUnicode "sub.example.com" `shouldBe` Right "sub.example.com"
    
    it "decodes mixed Punycode and ASCII" $ do
      case toUnicode "sub.xn--mnchen-3ya.de" of
        Right result -> do
          T.isInfixOf "sub" result `shouldBe` True
          T.isInfixOf "ü" result `shouldBe` True
        Left err -> expectationFailure $ "Failed: " ++ show err
    
    it "leaves non-Punycode labels unchanged" $ do
      toUnicode "example.com" `shouldBe` Right "example.com"
  
  describe "validateLabel" $ do
    it "accepts valid ASCII labels" $ do
      validateLabel "example" `shouldBe` Right ()
      validateLabel "ex-ample" `shouldBe` Right ()
      validateLabel "test123" `shouldBe` Right ()
    
    it "rejects empty labels" $ do
      validateLabel "" `shouldSatisfy` isLeft
    
    it "rejects labels starting with hyphen" $ do
      validateLabel "-example" `shouldSatisfy` isLeft
    
    it "rejects labels ending with hyphen" $ do
      validateLabel "example-" `shouldSatisfy` isLeft
    
    it "rejects labels with hyphens at positions 3-4 (unless xn--)" $ do
      validateLabel "ab--cd" `shouldSatisfy` isLeft
      validateLabel "xn--test" `shouldBe` Right ()
      validateLabel "xn--mnchen-3ya" `shouldBe` Right ()
    
    it "rejects labels longer than 63 octets" $ do
      let longLabel = T.replicate 64 "a"
      validateLabel longLabel `shouldSatisfy` isLeft
    
    it "accepts valid German Unicode label" $ do
      validateLabel "münchen" `shouldBe` Right ()
    
    it "accepts valid Japanese Unicode label" $ do
      validateLabel "日本" `shouldBe` Right ()
    
    it "accepts valid Chinese Unicode label" $ do
      validateLabel "中国" `shouldBe` Right ()
    
    it "accepts valid Arabic Unicode label" $ do
      validateLabel "مصر" `shouldBe` Right ()
    
    it "accepts valid Russian Unicode label" $ do
      validateLabel "испытание" `shouldBe` Right ()
    
    it "accepts valid Greek Unicode label" $ do
      validateLabel "ελλάδα" `shouldBe` Right ()
    
    it "accepts valid Korean Unicode label" $ do
      validateLabel "한국" `shouldBe` Right ()
    
    it "accepts valid Thai Unicode label" $ do
      validateLabel "ไทย" `shouldBe` Right ()
    
    it "accepts valid Hindi Unicode label" $ do
      validateLabel "भारत" `shouldBe` Right ()
    
    it "rejects labels starting with combining marks" $ do
      validateLabel "\x0301test" `shouldSatisfy` isLeft  -- COMBINING ACUTE ACCENT + test

  describe "round-trip ASCII ↔ Unicode" $ do
    it "round-trips German domain" $ do
      let original = "münchen.de"
      case toASCII original of
        Right ascii -> toUnicode ascii `shouldSatisfy` \r -> case r of
          Right unicode -> T.toLower unicode == T.toLower original
          Left _ -> False
        Left err -> expectationFailure $ "toASCII failed: " ++ show err
    
    it "round-trips Chinese domain" $ do
      let original = "中国.com"
      case toASCII original of
        Right ascii -> toUnicode ascii `shouldSatisfy` \r -> case r of
          Right unicode -> unicode == original
          Left _ -> False
        Left err -> expectationFailure $ "toASCII failed: " ++ show err
    
    it "round-trips pure ASCII domain" $ do
      let original = "example.com"
      case toASCII original of
        Right ascii -> toUnicode ascii `shouldBe` Right original
        Left err -> expectationFailure $ "toASCII failed: " ++ show err

  describe "Real-world domains" $ do
    it "handles Google Japan" $ do
      toASCII "グーグル.jp" `shouldSatisfy` isRight
    
    it "handles German government domains" $ do
      toASCII "münchen.de" `shouldSatisfy` isRight
      toASCII "düsseldorf.de" `shouldSatisfy` isRight
    
    it "handles Russian domains" $ do
      toASCII "яндекс.рф" `shouldSatisfy` isRight
    
    it "handles Arabic domains" $ do
      toASCII "مصر.مصر" `shouldSatisfy` isRight
    
    it "handles Chinese domains" $ do
      toASCII "中国.中国" `shouldSatisfy` isRight

isRight :: Either a b -> Bool
isRight (Right _) = True
isRight _ = False

isLeft :: Either a b -> Bool
isLeft (Left _) = True
isLeft _ = False