clod-0.2.3: test/Clod/FileSystem/TransformationsSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Clod.FileSystem.TransformationsSpec
-- Description : Tests for the FileSystem.Transformations module
-- Copyright : (c) Fuzz Leonard, 2025
-- License : MIT
-- Maintainer : fuzz@fuzz.ink
-- Stability : experimental
--
-- This module tests the file transformations functionality.
module Clod.FileSystem.TransformationsSpec (spec) where
import Test.Hspec
import Test.QuickCheck ()
import System.Directory (doesFileExist, createDirectory)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Control.Exception ()
import Control.Monad ()
import Control.Monad.Except ()
import Control.Monad.Reader ()
import qualified Data.ByteString as BS
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Either ()
import Data.Function ()
import Data.List ()
import Clod.Types
import Clod.FileSystem.Transformations
import Clod.FileSystem.Detection (needsTransformation)
import Clod.TestHelpers (defaultTestConfig)
-- | Test the file transformations
spec :: Spec
spec = do
describe "transformFilename" $ do
it "transforms SVG files to XML with special suffix" $ do
transformFilename "logo.svg" "logo.svg" `shouldBe` "logo-svg.xml"
it "leaves non-SVG files unchanged" $ do
transformFilename "main.js" "main.js" `shouldBe` "main.js"
transformFilename "index.html" "index.html" `shouldBe` "index.html"
it "transforms hidden files by removing dot and adding 'dot--' prefix" $ do
transformFilename ".gitignore" ".gitignore" `shouldBe` "dot--gitignore"
transformFilename ".tool-versions" ".tool-versions" `shouldBe` "dot--tool-versions"
transformFilename ".config" ".config" `shouldBe` "dot--config"
it "sanitizes filenames with special characters" $ do
transformFilename "file with spaces.txt" "file with spaces.txt" `shouldBe` "filewithtxt"
transformFilename "#weird$chars%.js" "#weird$chars%.js" `shouldBe` "weirdchars.js"
transformFilename "$$$.svg" "$$$.svg" `shouldBe` "-svg.xml"
it "returns a default name for empty filenames" $ do
transformFilename "" "" `shouldBe` "unnamed"
describe "flattenPath" $ do
it "replaces directory separators with underscores" $ do
flattenPath "dir/subdir/file.txt" `shouldBe` "dir_subdir_file.txt"
flattenPath "some\\windows\\path.txt" `shouldBe` "some_windows_path.txt"
it "leaves filenames without separators unchanged" $ do
flattenPath "file.txt" `shouldBe` "file.txt"
describe "sanitizeFilename" $ do
it "removes invalid characters from filenames" $ do
sanitizeFilename "hello world.txt" `shouldBe` "helloworld.txt"
sanitizeFilename "test!@#$%^&*().txt" `shouldBe` "test.txt"
sanitizeFilename "*special*.json" `shouldBe` "special.json"
it "preserves valid characters" $ do
sanitizeFilename "valid-name_123.js" `shouldBe` "valid-name_123.js"
sanitizeFilename "a.b.c.d.e.f" `shouldBe` "a.b.c.d.e.f"
it "returns 'unnamed' for empty strings" $ do
sanitizeFilename "" `shouldBe` "unnamed"
sanitizeFilename "#@$%^" `shouldBe` "unnamed"
describe "transformFileContent" $ do
it "transforms file content using the provided function" $ do
-- Create a temp directory
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create a test file
let srcPath = tmpDir </> "source.txt"
destPath = tmpDir </> "dest.txt"
content = "hello world"
transformFn = T.toUpper . TE.decodeUtf8
BS.writeFile srcPath (TE.encodeUtf8 (T.pack content))
-- Create capabilities
let readCap = fileReadCap [tmpDir]
writeCap = fileWriteCap [tmpDir]
config = defaultTestConfig tmpDir
-- Run the function
result <- runClodM config $
transformFileContent readCap writeCap transformFn srcPath destPath
-- Verify the destination file has the transformed content
result `shouldBe` Right ()
destContent <- TE.decodeUtf8 <$> BS.readFile destPath
destContent `shouldBe` "HELLO WORLD"
it "fails when source is outside read capability" $ do
-- Create a temp directory structure
withSystemTempDirectory "clod-test" $ \tmpDir -> do
withSystemTempDirectory "clod-test-outside" $ \outsideDir -> do
-- Create source file in the outside directory
let srcPath = outsideDir </> "source.txt"
destPath = tmpDir </> "dest.txt"
content = "hello world"
transformFn = T.toUpper . TE.decodeUtf8
BS.writeFile srcPath (TE.encodeUtf8 (T.pack content))
-- Create read capability that only includes the temp dir
let readCap = fileReadCap [tmpDir]
writeCap = fileWriteCap [tmpDir]
config = defaultTestConfig tmpDir
-- Run the function
result <- runClodM config $
transformFileContent readCap writeCap transformFn srcPath destPath
-- Verify the operation failed
case result of
Left (CapabilityError _ _) -> return ()
_ -> expectationFailure "Expected CapabilityError but got different result"
-- Verify the destination file wasn't created
destExists <- doesFileExist destPath
destExists `shouldBe` False
describe "SVG to XML transformation" $ do
it "preserves SVG content with XML extension" $ do
-- Create a temp directory structure
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create source and destination directories
let srcDir = tmpDir </> "src"
destDir = tmpDir </> "dest"
svgPath = srcDir </> "icon.svg"
xmlPath = destDir </> "icon-svg.xml"
svgContent = "<svg xmlns='http://www.w3.org/2000/svg'><circle cx='50' cy='50' r='40'/></svg>"
createDirectory srcDir
createDirectory destDir
BS.writeFile svgPath (TE.encodeUtf8 (T.pack svgContent))
-- Create capabilities
let readCap = fileReadCap [srcDir, destDir]
writeCap = fileWriteCap [destDir]
transformFn = TE.decodeUtf8 -- Transform ByteString to Text
config = defaultTestConfig tmpDir
-- Run the transformation
_ <- runClodM config $
transformFileContent readCap writeCap transformFn svgPath xmlPath
-- Verify the XML file was created with the same content
xmlContent <- TE.decodeUtf8 <$> BS.readFile xmlPath
T.unpack xmlContent `shouldBe` svgContent
describe "SPEC.md compliance" $ do
it "transforms filenames according to SPEC.md rules" $ do
-- Test examples from the SPEC.md document
-- Root files should be unchanged
transformFilename "package.json" "package.json" `shouldBe` "package.json"
-- Nested path files should be flattened with dashes
transformFilename "package.json" "app/lib/plugin/package.json" `shouldBe` "package.json"
-- Hidden files should have dots removed and replaced with dot-- prefix
transformFilename ".gitignore" ".gitignore" `shouldBe` "dot--gitignore"
transformFilename ".env" ".env" `shouldBe` "dot--env"
-- SVG files should be transformed to XML
transformFilename "logo.svg" "logo.svg" `shouldBe` "logo-svg.xml"
-- For nested SVG files, the function uses the filename (first arg), not the path (second arg)
transformFilename "logo.svg" "assets/images/logo.svg" `shouldBe` "logo-svg.xml"
it "correctly identifies files needing transformation" $ do
needsTransformation ".gitignore" `shouldBe` True
needsTransformation ".env" `shouldBe` True
needsTransformation "logo.svg" `shouldBe` True
needsTransformation "regular.txt" `shouldBe` False
describe "End-to-end transformation" $ do
it "works with complex transformations" $ do
-- Create a temp directory
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create test files
let jsPath = tmpDir </> "script.js"
htmlPath = tmpDir </> "page.html"
jsContent = "console.log('Hello, world!');"
htmlContent = "<html><body><h1>Test</h1></body></html>"
BS.writeFile jsPath (TE.encodeUtf8 (T.pack jsContent))
BS.writeFile htmlPath (TE.encodeUtf8 (T.pack htmlContent))
-- Create capabilities
let readCap = fileReadCap [tmpDir]
writeCap = fileWriteCap [tmpDir]
config = defaultTestConfig tmpDir
-- Define a simple transformation function that adds different comments for different file types
let jsTransform :: BS.ByteString -> T.Text
jsTransform bs = "// Transformed by Clod\n" <> TE.decodeUtf8 bs
htmlTransform :: BS.ByteString -> T.Text
htmlTransform bs = "<!-- Transformed by Clod -->\n" <> TE.decodeUtf8 bs
-- Run transformations
let jsDest = tmpDir </> "script.transformed.js"
htmlDest = tmpDir </> "page.transformed.html"
_ <- runClodM config $ transformFileContent readCap writeCap jsTransform jsPath jsDest
_ <- runClodM config $ transformFileContent readCap writeCap htmlTransform htmlPath htmlDest
-- Verify results
jsResult <- TE.decodeUtf8 <$> BS.readFile jsDest
htmlResult <- TE.decodeUtf8 <$> BS.readFile htmlDest
jsResult `shouldBe` "// Transformed by Clod\n" <> T.pack jsContent
htmlResult `shouldBe` "<!-- Transformed by Clod -->\n" <> T.pack htmlContent