packages feed

salmon-ops-recipes-0.1.0.0: test/Test/JWTSigningSpec.hs

-- | Layer 0 (structural) + Layer 1 (sandboxed real IO) tests for
-- "SreBox.JWTSigning", picked as the first target because it is
-- self-contained: it only touches the filesystem, no daemons/root needed.
module Test.JWTSigningSpec (tests) where

import Control.Comonad.Cofree (Cofree (..))
import qualified Data.ByteString.Char8 as C8
import qualified Salmon.Actions.UpDown as UpDown
import qualified Salmon.Builtin.Nodes.Secrets as Secrets
import Salmon.Builtin.Extension (Op, evalDeps, ignoreTrack)
import Salmon.Op.Track (pureTracked)
import Salmon.Reporter (silent)
import qualified SreBox.JWTSigning as JWT
import System.Directory (doesFileExist)
import System.FilePath ((</>))
import Test.Harness
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, testCase, (@?=))

tests :: TestTree
tests =
    testGroup
        "SreBox.JWTSigning"
        [ testCase "Layer 0: ref is stable and derived from the output path" layer0Ref
        , testCase "Layer 1: up writes a token file, a second up is a no-op" layer1UpIsIdempotent
        ]

-- A secret that is entirely test-managed: we write the raw key bytes
-- ourselves and hand it in via 'ignoreTrack', so the op under test has no
-- dependency to provision (mirrors how a real caller would pass in an
-- already-existing secret file).
mkOp :: FilePath -> FilePath -> Op
mkOp secretPath jwtPath =
    JWT.signHmac silent secretTracked "{\"sub\":\"test\"}" jwtPath
  where
    secret = Secrets.Secret Secrets.Hex 32 secretPath
    secretTracked = pureTracked ignoreTrack secret

layer0Ref :: IO ()
layer0Ref = do
    let o1 = mkOp "/tmp/does-not-matter-key" "/tmp/a.jwt"
        o2 = mkOp "/tmp/does-not-matter-key" "/tmp/a.jwt"
        o3 = mkOp "/tmp/does-not-matter-key" "/tmp/b.jwt"
        r1 :< _ = evalDeps o1
        r2 :< _ = evalDeps o2
        r3 :< _ = evalDeps o3
    -- same output path => same identity (so upTree dedups correctly)
    assertEqual "same jwtPath yields the same node" (show r1) (show r2)
    -- different output path => different identity
    assertBool "different jwtPath yields a different node" (show r1 /= show r3)

layer1UpIsIdempotent :: IO ()
layer1UpIsIdempotent = withTempDir $ \dir -> do
    let secretPath = dir </> "hmac.key"
        jwtPath = dir </> "token.jwt"
    -- test manages the secret material directly: 32 raw bytes, hex-decodable
    C8.writeFile secretPath (C8.replicate 64 'a')

    let o = mkOp secretPath jwtPath

    -- first up: the file doesn't exist yet, check must report Failure, and the
    -- token gets written for real
    reports1 <- runUpCapturing o
    assertBool "first up evaluates (file did not exist)" (any isEval reports1)
    exists1 <- doesFileExist jwtPath
    exists1 @?= True

    -- second up: skipIfFileExists now sees the file, check must skip, and
    -- content is left untouched (no re-signing)
    contentsAfterFirstUp <- C8.readFile jwtPath
    reports2 <- runUpCapturing o
    assertBool "second up is skipped (file now exists)" (any isSkip reports2 && not (any isEval reports2))
    contentsAfterSecondUp <- C8.readFile jwtPath
    contentsAfterSecondUp @?= contentsAfterFirstUp
  where
    isEval (UpDown.Eval _) = True
    isEval _ = False
    isSkip (UpDown.Skip _) = True
    isSkip _ = False