packages feed

plexus-synapse-3.15.0: test/SchemaDecodeSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

-- | REQ-S10 spike: verify Plexus.Schema.Recursive.PluginSchema decodes
-- the new optional psRequest field losslessly, including x-plexus-source
-- extension nodes.
module Main where

import Data.Aeson
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import Data.Aeson.QQ.Simple (aesonQQ)
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Maybe (isJust, isNothing, fromJust)
import qualified Data.Vector as V
import System.Exit (exitFailure, exitSuccess)
import qualified Data.IORef as IORef

import Plexus.Schema.Recursive (PluginSchema(..))

-- ============================================================================
-- Fixtures
-- ============================================================================

fixtureWithRequest :: Value
fixtureWithRequest = [aesonQQ|{
  "namespace": "clients",
  "version": "1.0.0",
  "description": "Client management",
  "hash": "abc123",
  "methods": [],
  "request": {
    "type": "object",
    "properties": {
      "auth_token": {
        "type": "string",
        "description": "JWT from Keycloak",
        "x-plexus-source": { "from": "cookie", "key": "access_token" }
      },
      "origin": {
        "type": "string",
        "x-plexus-source": { "from": "header", "key": "origin" }
      },
      "peer_addr": {
        "type": "string",
        "x-plexus-source": { "from": "derived" }
      }
    },
    "required": ["auth_token"]
  }
}|]

fixtureWithoutRequest :: Value
fixtureWithoutRequest = [aesonQQ|{
  "namespace": "forms",
  "version": "1.0.0",
  "description": "Forms (no auth required)",
  "hash": "def456",
  "methods": []
}|]

fixtureEmptyRequest :: Value
fixtureEmptyRequest = [aesonQQ|{
  "namespace": "edge",
  "version": "1.0.0",
  "description": "Edge case",
  "hash": "ghi789",
  "methods": [],
  "request": {}
}|]

-- ============================================================================
-- Test runner
-- ============================================================================

data TestResult = Pass String | Fail String String

runTest :: String -> Bool -> String -> TestResult
runTest name True  _   = Pass name
runTest name False why = Fail name why

main :: IO ()
main = do
  failures <- IORef.newIORef (0 :: Int)
  let run :: TestResult -> IO ()
      run (Pass name)        = putStrLn $ "  [PASS] " <> name
      run (Fail name reason) = do
        putStrLn $ "  [FAIL] " <> name <> ": " <> reason
        IORef.modifyIORef' failures (+ 1)

  putStrLn "=== REQ-S10: PluginSchema psRequest decoding ==="

  -- Pass condition 1: fixtureWithRequest decodes to psRequest = Just _
  -- and x-plexus-source.from on auth_token is "cookie"
  case fromJSON fixtureWithRequest :: Result PluginSchema of
    Error e -> run (Fail "fixture-with-request decodes" e)
    Success schema -> do
      run $ runTest "fixture-with-request: psRequest = Just _"
              (isJust (psRequest schema))
              "expected Just, got Nothing"
      let cookieFrom = do
            req <- psRequest schema
            obj <- valueToObject req
            props <- KeyMap.lookup "properties" obj
            propsObj <- valueToObject props
            authTok <- KeyMap.lookup "auth_token" propsObj
            atObj <- valueToObject authTok
            src <- KeyMap.lookup "x-plexus-source" atObj
            srcObj <- valueToObject src
            from <- KeyMap.lookup "from" srcObj
            valueToString from
      run $ runTest "fixture-with-request: auth_token x-plexus-source.from == 'cookie'"
              (cookieFrom == Just "\"cookie\"")
              ("got: " <> show cookieFrom)

  -- Pass condition 2: fixtureWithoutRequest decodes to psRequest = Nothing
  case fromJSON fixtureWithoutRequest :: Result PluginSchema of
    Error e -> run (Fail "fixture-without-request decodes" e)
    Success schema ->
      run $ runTest "fixture-without-request: psRequest = Nothing"
              (isNothing (psRequest schema))
              ("expected Nothing, got: " <> show (psRequest schema))

  -- Pass condition 3: fixtureEmptyRequest decodes to psRequest = Just (Object empty)
  case fromJSON fixtureEmptyRequest :: Result PluginSchema of
    Error e -> run (Fail "fixture-empty-request decodes" e)
    Success schema -> do
      let isEmptyObject = case psRequest schema of
            Just (Object km) -> KeyMap.null km
            _                -> False
      run $ runTest "fixture-empty-request: psRequest = Just (Object empty)"
              isEmptyObject
              ("got: " <> show (psRequest schema))

  -- Pass condition 4: lossless roundtrip on all three fixtures
  let roundtripCheck name fixture =
        case fromJSON fixture :: Result PluginSchema of
          Error e -> Fail (name <> " decode for roundtrip") e
          Success original ->
            case decode (encode original) :: Maybe PluginSchema of
              Nothing -> Fail (name <> " roundtrip decode") "encode/decode failed"
              Just decoded ->
                runTest (name <> " roundtrip lossless")
                        (decoded == original)
                        "decoded /= original"

  run $ roundtripCheck "fixture-with-request" fixtureWithRequest
  run $ roundtripCheck "fixture-without-request" fixtureWithoutRequest
  run $ roundtripCheck "fixture-empty-request" fixtureEmptyRequest

  -- Pass condition 5: x-plexus-source survives roundtrip on all three field types
  case fromJSON fixtureWithRequest :: Result PluginSchema of
    Error _ -> run (Fail "x-plexus-source roundtrip" "decode failed")
    Success original -> do
      let reEncoded = encode original
      case decode reEncoded :: Maybe PluginSchema of
        Nothing -> run (Fail "x-plexus-source roundtrip decode" "decode-after-encode failed")
        Just decoded -> do
          let extractFrom propName = do
                req <- psRequest decoded
                obj <- valueToObject req
                props <- KeyMap.lookup "properties" obj >>= valueToObject
                prop <- KeyMap.lookup (Key.fromString propName) props >>= valueToObject
                src <- KeyMap.lookup "x-plexus-source" prop >>= valueToObject
                from <- KeyMap.lookup "from" src
                valueToString from
          run $ runTest "x-plexus-source roundtrip: cookie field"
                  (extractFrom "auth_token" == Just "\"cookie\"") (show $ extractFrom "auth_token")
          run $ runTest "x-plexus-source roundtrip: header field"
                  (extractFrom "origin" == Just "\"header\"") (show $ extractFrom "origin")
          run $ runTest "x-plexus-source roundtrip: derived field"
                  (extractFrom "peer_addr" == Just "\"derived\"") (show $ extractFrom "peer_addr")

  fc <- IORef.readIORef failures
  if fc == 0
    then do
      putStrLn "\nREQ-S10: ALL TESTS PASSED"
      exitSuccess
    else do
      putStrLn $ "\nREQ-S10: " <> show fc <> " test(s) failed"
      exitFailure

-- helpers
valueToObject :: Value -> Maybe Object
valueToObject (Object o) = Just o
valueToObject _          = Nothing

valueToString :: Value -> Maybe String
valueToString (String t) = Just (show t)  -- show wraps in quotes; we compare against quoted strings below
valueToString _          = Nothing