packages feed

clod-0.2.3: test/Clod/TypesSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |
-- Module      : Clod.TypesSpec
-- Description : Tests for core types
-- Copyright   : (c) Fuzz Leonard, 2025
-- License     : MIT
-- Maintainer  : fuzz@fuzz.ink
-- Stability   : experimental
--
-- This module contains tests for the core types of the Clod application.

module Clod.TypesSpec (spec) where

import Test.Hspec
import Test.QuickCheck hiding (Success)
import Data.Text ()
import System.IO.Temp ()
import Control.Monad.Reader ()
import Control.Monad.Except ()

import Clod.Types
import Clod.TestHelpers (defaultTestConfig)

-- | Property: OptimizedName should preserve its structure through the newtype
prop_optimizedNameRoundTrip :: String -> Bool
prop_optimizedNameRoundTrip s = unOptimizedName (OptimizedName s) == s

-- | Property: OriginalPath should preserve its structure through the newtype
prop_originalPathRoundTrip :: FilePath -> Bool 
prop_originalPathRoundTrip p = unOriginalPath (OriginalPath p) == p

-- | Property: Validated to Either should preserve success values
prop_validatedToEitherSuccess :: Int -> Bool
prop_validatedToEitherSuccess x = validatedToEither (Valid x) == Right x

-- | Property: Either to Validated should preserve Right values
prop_eitherToValidatedRight :: Int -> Bool
prop_eitherToValidatedRight x = eitherToValidated (Right x) == Valid x

spec :: Spec
spec = do
  describe "Newtypes and smart constructors" $ do
    it "OptimizedName preserves the original string" $ do
      property prop_optimizedNameRoundTrip
      
    it "OriginalPath preserves the original path" $ do
      property prop_originalPathRoundTrip
      
    it "can create an optimized name" $ do
      let name = OptimizedName "test.txt"
      unOptimizedName name `shouldBe` "test.txt"
      
    it "can create an original path" $ do
      let path = OriginalPath "/path/to/file.txt"
      unOriginalPath path `shouldBe` "/path/to/file.txt"
  
  describe "ClodError" $ do
    it "can be created and displayed" $ do
      show (ConfigError "test error") `shouldBe` "ConfigError \"test error\""
      show (DatabaseError "db.dhall" (DBCorrupted "parse error")) `shouldBe` "DatabaseError \"db.dhall\" (DBCorrupted \"parse error\")"
      show (CapabilityError "/path/to/file" "Access denied") `shouldBe` "CapabilityError \"/path/to/file\" \"Access denied\""
      show (ChecksumError "data.bin" "Binary file") `shouldBe` "ChecksumError \"data.bin\" \"Binary file\""
      
    it "handles all database error types" $ do
      show (DatabaseError "db" DBFileNotFound) `shouldBe` "DatabaseError \"db\" DBFileNotFound"
      show (DatabaseError "db" (DBCorrupted "parse error")) `shouldBe` "DatabaseError \"db\" (DBCorrupted \"parse error\")"
      show (DatabaseError "db" DBVersionMismatch) `shouldBe` "DatabaseError \"db\" DBVersionMismatch"
      show (DatabaseError "db" (DBOtherError "unknown error")) `shouldBe` "DatabaseError \"db\" (DBOtherError \"unknown error\")"
  
  describe "FileResult" $ do
    it "can be created and displayed" $ do
      show (Success) `shouldBe` "Success"
      show (Skipped "reason") `shouldBe` "Skipped \"reason\""
  
  describe "ClodM Monad Stack" $ do
    it "handles reader operations correctly" $ do
      let mtlComputation = do
            config <- ask
            return (config ^. projectPath) :: ClodM String
            
      mtlResult <- runExceptT $ runReaderT mtlComputation (defaultTestConfig "test-dir")
      mtlResult `shouldBe` Right "test-dir"
      
    it "handles errors correctly" $ do
      let mtlError = throwError (ConfigError "test error") :: ClodM String
          
      mtlResult <- runExceptT $ runReaderT mtlError (defaultTestConfig "test")
      mtlResult `shouldBe` Left (ConfigError "test error")
      
  describe "Validated data type" $ do
    it "converts from Valid to Right" $ do
      property prop_validatedToEitherSuccess
      
    it "converts from Invalid to Left (first error)" $ do
      -- Test with a specific case instead of property
      let e1 = ConfigError "main error"
      let e2 = ConfigError "secondary error"
      validatedToEither (Invalid [e1, e2] :: Validated String) `shouldBe` (Left e1 :: Either ClodError String)
      
    it "converts from Right to Valid" $ do
      property prop_eitherToValidatedRight
      
    it "converts from Left to Invalid (singleton)" $ do
      -- Test with a specific case
      let err = ConfigError "test error"
      eitherToValidated (Left err :: Either ClodError String) `shouldBe` (Invalid [err] :: Validated String)
      
    it "combines errors when applying Invalid values" $ do
      -- Test with specific cases
      let es1 = [ConfigError "error 1"]
      let es2 = [ConfigError "error 2"]
      let v1 = Invalid es1 :: Validated (Int -> Int)
      let v2 = Invalid es2 :: Validated Int
      
      case v1 <*> v2 of
        Invalid combined -> combined `shouldBe` es1 ++ es2
        _ -> expectationFailure "Expected Invalid result"
      
    it "handles basic Applicative operations correctly" $ do
      let v1 = Valid (+1) :: Validated (Int -> Int)
      let v2 = Valid 2 :: Validated Int
      
      (v1 <*> v2) `shouldBe` Valid 3
      
      let e1 = Invalid [ConfigError "error 1"] :: Validated (Int -> Int)
      let e2 = Invalid [ConfigError "error 2"] :: Validated Int
      
      (v1 <*> e2) `shouldBe` e2
      
      -- Can't directly compare here because types differ - test the core behavior instead
      case (e1 <*> v2) of
        Invalid [ConfigError msg] -> msg `shouldBe` "error 1"
        _ -> expectationFailure "Expected Invalid with ConfigError"
      
      -- Test the combination of errors
      (e1 <*> e2) `shouldBe` Invalid [ConfigError "error 1", ConfigError "error 2"]