clod-0.2.3: test/Clod/CoreSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
-- |
-- Module : Clod.CoreSpec
-- Description : Tests for Core module functionality
-- Copyright : (c) Fuzz Leonard, 2025
-- License : MIT
-- Maintainer : fuzz@fuzz.ink
-- Stability : experimental
--
-- This module contains tests for core Clod functionality.
module Clod.CoreSpec (spec) where
import Test.Hspec
import System.Directory (createDirectoryIfMissing, doesDirectoryExist,
doesFileExist, getDirectoryContents)
import System.FilePath
import System.IO.Temp (withSystemTempDirectory)
import Data.Either (isRight)
import Control.Monad (when)
import Data.List (isInfixOf)
import Clod.FileSystem.Checksums (checksumFile)
import Clod.Core
import Clod.Types
( FileResult(..), IgnorePattern(..),
runClodM, isPathAllowed, fileReadCap, fileWriteCap,
ignorePatterns, allowedReadDirs, (&), (.~), (^.)
)
import Clod.TestHelpers (defaultTestConfig)
-- | Test specification for Core module
spec :: Spec
spec = do
fileProcessingSpec
runClodAppSpec
ignorePatternSpec
-- | Tests for the file processing functionality
fileProcessingSpec :: Spec
fileProcessingSpec = describe "File processing with ClodM" $ do
it "can process text files with capabilities" $ do
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create test directories and files
createDirectoryIfMissing True (tmpDir </> "src")
createDirectoryIfMissing True (tmpDir </> "staging")
writeFile (tmpDir </> "src" </> "test.txt") "Test file"
-- Create a test config
let config = defaultTestConfig tmpDir
-- Create limited capabilities (only src directory for read, staging for write)
let readCap = fileReadCap [tmpDir </> "src"]
writeCap = fileWriteCap [tmpDir </> "staging"]
-- Process a file
result <- runClodM config $
processFile readCap writeCap (tmpDir </> "src" </> "test.txt") "src/test.txt"
-- Should succeed
case result of
Left err -> expectationFailure $ "Failed to process file: " ++ show err
Right fileResult -> fileResult `shouldBe` Success
-- Verify the file was copied to staging
copiedExists <- doesFileExist (tmpDir </> "staging" </> "test.txt")
copiedExists `shouldBe` True
it "respects capability restrictions" $ do
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create directories
createDirectoryIfMissing True (tmpDir </> "src")
createDirectoryIfMissing True (tmpDir </> "private")
createDirectoryIfMissing True (tmpDir </> "staging")
-- Create test files
writeFile (tmpDir </> "src" </> "test.txt") "Public test file"
writeFile (tmpDir </> "private" </> "secret.txt") "Secret file"
-- Create a test config
let config = defaultTestConfig tmpDir
-- Create limited capabilities (only src directory, not private)
let readCap = fileReadCap [tmpDir </> "src"]
writeCap = fileWriteCap [tmpDir </> "staging"]
-- Try to access file outside allowed directory
result <- runClodM config $
processFile readCap writeCap (tmpDir </> "private" </> "secret.txt") "private/secret.txt"
-- Should fail due to capability restriction
result `shouldSatisfy` isLeft
where
isLeft (Left _) = True
isLeft _ = False
-- | Tests for runClodApp
runClodAppSpec :: Spec
runClodAppSpec = describe "runClodApp" $ do
it "initializes paths correctly" $ do
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create a git repo structure
createDirectoryIfMissing True (tmpDir </> ".git")
-- Create a test config
let config = defaultTestConfig tmpDir
-- Run initialization function
result <- runClodApp config "" False False
-- Should succeed
result `shouldSatisfy` isRight
-- Check if directories were created
stagingExists <- doesDirectoryExist (tmpDir </> "staging")
stagingExists `shouldBe` True
configDirExists <- doesDirectoryExist (tmpDir </> ".clod")
configDirExists `shouldBe` True
it "updates the database with staging directory" $ do
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create a git repo structure
createDirectoryIfMissing True (tmpDir </> ".git")
createDirectoryIfMissing True (tmpDir </> ".clod")
-- Create a test config
let config = defaultTestConfig tmpDir
dbPath = tmpDir </> ".clod" </> "db.dhall"
-- Run application
result <- runClodApp config "" False False
-- Should succeed
result `shouldSatisfy` isRight
-- Check if database file was created
dbExists <- doesFileExist dbPath
dbExists `shouldBe` True
-- We can't easily check the database contents directly since we've simplified our database
-- implementation for testing, but we can verify the database file exists
True `shouldBe` True
it "properly honors capability restrictions" $ do
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create a git repo structure with a forbidden directory outside the repo
createDirectoryIfMissing True (tmpDir </> ".git")
let forbiddenDir = "/tmp/forbidden" -- A directory outside our capability
-- Create a test config (unused in this test but kept for consistency)
let _config = defaultTestConfig tmpDir
-- Test that our capability restricts access as expected
let readCap = fileReadCap [tmpDir]
-- Try to check if a file exists outside our capability
allowed <- isPathAllowed (readCap ^. allowedReadDirs) forbiddenDir
allowed `shouldBe` False
-- | Tests for ignore pattern handling
ignorePatternSpec :: Spec
ignorePatternSpec = describe "Ignore pattern handling" $ do
it "does not checksum files matching ignore patterns" $ do
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create directories and test files
createDirectoryIfMissing True (tmpDir </> "src")
createDirectoryIfMissing True (tmpDir </> "ignored")
createDirectoryIfMissing True (tmpDir </> ".clod")
-- Create test files
writeFile (tmpDir </> "src" </> "main.txt") "This is a valid text file"
writeFile (tmpDir </> "ignored" </> "ignored.txt") "This file should be ignored"
-- Create a .clodignore file
writeFile (tmpDir </> ".clodignore") "ignored/"
-- Create a test config with ignore patterns
let config = defaultTestConfig tmpDir &
ignorePatterns .~ [IgnorePattern "ignored/"]
-- Create the read capability
let readCap = fileReadCap [tmpDir]
-- First verify that without ignore patterns, we would checksum both files
-- by directly using checksumFile
resultMain <- runClodM config $ checksumFile readCap (tmpDir </> "src" </> "main.txt")
resultIgnored <- runClodM config $ checksumFile readCap (tmpDir </> "ignored" </> "ignored.txt")
-- Both files can be checksummed individually as text files
resultMain `shouldSatisfy` isRight
resultIgnored `shouldSatisfy` isRight
-- Now run the mainLogic function which should respect the ignore patterns
result <- runClodApp config "" False True
-- Check if it worked
case result of
Left err -> expectationFailure $ "Failed to run clod: " ++ show err
Right _ -> do
-- Check if ignore patterns were respected
let normalFile = tmpDir </> "staging" </> "src-main.txt"
ignoredFile = tmpDir </> "staging" </> "ignored-ignored.txt"
manifestPath = tmpDir </> "staging" </> "_path_manifest.dhall"
-- Check which files exist in staging
normalExists <- doesFileExist normalFile
ignoredExists <- doesFileExist ignoredFile
manifestExists <- doesFileExist manifestPath
-- The regular file should be copied
normalExists `shouldBe` True
-- The ignored file should NOT be copied
ignoredExists `shouldBe` False
-- The manifest should exist
manifestExists `shouldBe` True
-- Read manifest content to verify ignored file is not included
manifestContent <- readFile manifestPath
-- The manifest should contain the normal file
manifestContent `shouldContain` "src/main.txt"
-- The manifest should NOT contain the ignored file
manifestContent `shouldNotContain` "ignored/ignored.txt"
-- Assert that normal file IS copied to staging
normalExists `shouldBe` True
-- Assert that ignored file is NOT copied to staging
ignoredExists `shouldBe` False
it "excludes ignored files from checksum database" $ do
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create directories and test files
createDirectoryIfMissing True (tmpDir </> "src")
createDirectoryIfMissing True (tmpDir </> ".git")
createDirectoryIfMissing True (tmpDir </> "node_modules")
createDirectoryIfMissing True (tmpDir </> ".clod")
-- Create test files
writeFile (tmpDir </> "src" </> "main.txt") "Normal file"
writeFile (tmpDir </> ".git" </> "HEAD") "ref: refs/heads/main"
writeFile (tmpDir </> "node_modules" </> "package.js") "module.exports = {}"
-- Create a .clodignore file
writeFile (tmpDir </> ".clodignore") ".git/\nnode_modules/"
-- Create a test config with ignore patterns
let config = defaultTestConfig tmpDir &
ignorePatterns .~ [IgnorePattern ".git/", IgnorePattern "node_modules/"]
-- Run the application
result <- runClodApp config "" True False
-- Check if it worked
case result of
Left err -> expectationFailure $ "Failed to run clod: " ++ show err
Right _ -> do
-- Check the database file
let dbPath = tmpDir </> ".clod" </> "db.dhall"
dbExists <- doesFileExist dbPath
-- The database should exist
dbExists `shouldBe` True
-- Check the database content
when dbExists $ do
dbContent <- readFile dbPath
-- The database should contain the normal file
dbContent `shouldContain` "src/main.txt"
-- The database should NOT contain ignored files
dbContent `shouldNotContain` "\".git/HEAD\""
dbContent `shouldNotContain` "\"node_modules/package.js\""
it "respects .clodignore and .gitignore patterns for copying" $ do
withSystemTempDirectory "clod-test" $ \tmpDir -> do
-- Create directories and test files
createDirectoryIfMissing True (tmpDir </> ".git")
createDirectoryIfMissing True (tmpDir </> "src")
createDirectoryIfMissing True (tmpDir </> "node_modules")
createDirectoryIfMissing True (tmpDir </> "staging")
-- Create test files
writeFile (tmpDir </> "src" </> "main.js") "console.log('test');"
writeFile (tmpDir </> "node_modules" </> "package.js") "module.exports = {};"
writeFile (tmpDir </> ".git" </> "HEAD") "ref: refs/heads/main"
-- Create a .clodignore file (both in root and .clod dir for compatibility)
createDirectoryIfMissing True (tmpDir </> ".clod")
writeFile (tmpDir </> ".clodignore") "node_modules/\n.git/"
writeFile (tmpDir </> ".clod" </> ".clodignore") "node_modules/\n.git/"
-- Create a fake git repo to ensure Git detection works
createDirectoryIfMissing True (tmpDir </> ".git" </> "objects")
createDirectoryIfMissing True (tmpDir </> ".git" </> "refs")
writeFile (tmpDir </> ".git" </> "config")
"[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n"
-- Create a test config that sets the ignorePatterns manually
-- This is critical - we need to set the patterns explicitly
let config = defaultTestConfig tmpDir &
ignorePatterns .~ [IgnorePattern "node_modules/", IgnorePattern ".git/"] -- Set patterns directly
-- Call Core.runClodApp directly, which should use the ignorePatterns in the config
result <- runClodApp config "" False True
-- Check if it worked
case result of
Left err -> expectationFailure $ "Failed to run clod: " ++ show err
Right _ -> do
-- Directly check if the ignored files were copied
let nodeModulesJs = tmpDir </> "staging" </> "node_modules-package.js"
gitFile = tmpDir </> "staging" </> ".git-HEAD"
manifestPath = tmpDir </> "staging" </> "_path_manifest.dhall"
-- Get all files in the staging directory
allFiles <- getDirectoryContents (tmpDir </> "staging")
-- Try to find the main.js file regardless of its exact name
-- This makes the test more resilient to different optimized filename implementations
let mainFiles = filter (\f -> "main" `isInfixOf` f && f `notElem` [".", ".."]) allFiles
let hasMainFile = not (null mainFiles)
-- Check if node_modules or git files were copied (they shouldn't be)
nodeModulesCopied <- doesFileExist nodeModulesJs
gitFileCopied <- doesFileExist gitFile
-- Check if the manifest exists
manifestExists <- doesFileExist manifestPath
-- Run assertions
-- The src/main.js file should be copied (or any file with 'main' in the name)
hasMainFile `shouldBe` True
-- The node_modules/package.js file should NOT be copied (ignored)
nodeModulesCopied `shouldBe` False
-- The .git/HEAD file should NOT be copied (ignored)
gitFileCopied `shouldBe` False
-- The manifest should exist
manifestExists `shouldBe` True
-- Check manifest contents
when manifestExists $ do
manifestContent <- readFile manifestPath
-- The manifest should contain src/main.js
manifestContent `shouldContain` "src/main.js"
-- The manifest should NOT contain any .git files
manifestContent `shouldNotContain` ".git/HEAD"
manifestContent `shouldNotContain` ".git/config"
-- Also check the database file to verify it doesn't contain git files
let dbPath = tmpDir </> ".clod" </> "db.dhall"
dbExists <- doesFileExist dbPath
-- If the database exists, check its contents
when dbExists $ do
dbContent <- readFile dbPath
-- The database should not contain any git files
dbContent `shouldNotContain` "\".git/HEAD\""
dbContent `shouldNotContain` "\".git/config\""