packages feed

plexus-synapse-3.15.0: test/Req8RenderSpec.hs

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

-- | REQ-8 acceptance: synapse renderer shows per-param x-plexus-source
-- annotations inline when rendering a method's help.
--
-- SYNAPSE-13: a listing no longer shows parameters (one line per method),
-- so the annotations are pinned on the method's page, built from the IR.
module Main where

import Data.Aeson (Value)
import Data.Aeson.QQ.Simple (aesonQQ)
import qualified Data.IORef as IORef
import qualified Data.Text as T
import Data.Text (Text)
import System.Exit (exitFailure, exitSuccess)

import qualified Plexus.Schema.Recursive
import Plexus.Schema.Recursive
  ( PluginSchema(..), MethodSchema(..) )
import qualified Data.Map.Strict as Map
import Synapse.CLI.Help (renderMethodHelp)
import Synapse.IR.Builder (extractMethodDef)
import Synapse.IR.Types (IR(..), MethodDef(..), emptyIR)

-- ─── Fixture ──────────────────────────────────────────────────

-- A plugin with one method that has three params:
--   - user: x-plexus-source.from = auth (resolver)
--   - origin: x-plexus-source.from = derived
--   - search: no annotation (RPC param)
fixture :: PluginSchema
fixture = PluginSchema
  { psNamespace       = "svc"
  , psVersion         = "1.0.0"
  , psDescription     = "Test service"
  , psLongDescription = Nothing
  , psHash            = "h1"
  , psMethods         = [method1]
  , psChildren        = Nothing
  , psDeprecation     = Nothing
  , psRoles = Nothing, psView = Nothing
  }
  where
    method1 = MethodSchema
      { methodName          = "list"
      , methodDescription   = "List things"
      , methodHash          = "h2"
      , methodParams        = Just paramsSchema
      , methodReturns       = Nothing
      , methodStreaming     = False
      , methodBidirectional = False
      , methodRequestType   = Nothing
      , methodResponseType  = Nothing
      , methodDeprecation   = Nothing
      , methodParamSchemas  = Nothing
      , methodRole          = defaultRole
      , methodLongDescription = Nothing, methodGrant = Nothing, methodTarget = Nothing, methodHeldOn = Nothing, methodPublic = False, methodGroup = Nothing, methodExamples = []
      }

-- | Stub role — renderSchema doesn't branch on role for this test.
defaultRole :: Plexus.Schema.Recursive.MethodRole
defaultRole = Plexus.Schema.Recursive.MethodRoleRpc

paramsSchema :: Value
paramsSchema = [aesonQQ|{
  "type": "object",
  "properties": {
    "user": {
      "type": "object",
      "x-plexus-source": {
        "from": "auth",
        "resolver": "self.db.validate_user"
      }
    },
    "origin": {
      "type": "object",
      "x-plexus-source": { "from": "derived" }
    },
    "search": {
      "type": "string"
    }
  },
  "required": ["search"]
}|]

-- ─── Runner ───────────────────────────────────────────────────

data TestResult = Pass String | Fail String String

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

  let (types, md) = extractMethodDef "svc" "svc" (head (psMethods fixture))
      ir = emptyIR { irMethods = Map.singleton (mdFullPath md) md, irTypes = types }
      out = renderMethodHelp ir md

  putStrLn "=== REQ-8: per-param source annotations in renderer ==="
  putStrLn "(rendered output)"
  putStrLn "---"
  putStrLn $ T.unpack out
  putStrLn "---"

  run $ assertContains out "list"    "method name appears"
  run $ assertContains out "user"    "auth-sourced param name appears"
  run $ assertContains out "origin"  "derived-sourced param name appears"
  run $ assertContains out "--search" "RPC param emitted as --flag"
  run $ assertContains out "auth: self.db.validate_user"
           "auth resolver expression appears on the auth-sourced param"
  run $ assertContains out "server-derived"
           "derived source is labeled 'server-derived'"
  run $ assertNotContains out "--user"
           "auth-sourced param is NOT emitted as --flag (server-extracted)"
  run $ assertNotContains out "--origin"
           "derived-sourced param is NOT emitted as --flag"

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

assertContains :: Text -> Text -> String -> TestResult
assertContains hay needle name =
  if needle `T.isInfixOf` hay
    then Pass name
    else Fail name ("expected output to contain: " <> T.unpack needle)

assertNotContains :: Text -> Text -> String -> TestResult
assertNotContains hay needle name =
  if needle `T.isInfixOf` hay
    then Fail name ("unexpected substring present: " <> T.unpack needle)
    else Pass name