packages feed

synapse-cc-0.4.0: test/AuthCheckSpec.hs

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

-- | SAFE-6 acceptance tests for SynapseCC.AuthCheck.
module Main where

import Data.Aeson (Value, eitherDecodeStrict)
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 System.FilePath ((</>))
import qualified System.IO.Temp as Temp
import qualified Data.Text.IO as TIO
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Aeson as Aeson

import SynapseCC.AuthCheck (backendRequiresCookieAuth, detectCookieAuthMismatch)

-- ─── Fixtures ─────────────────────────────────────────────────

irWithAuthMethod :: Value
irWithAuthMethod = [aesonQQ|{
  "irVersion": "2.0",
  "irBackend": "test",
  "irMethods": {
    "svc.list": {
      "mdName": "list",
      "mdFullPath": "svc.list",
      "mdNamespace": "svc",
      "mdParams": [
        { "pdName": "user",
          "pdType": {"tag":"RefPrimitive","contents":["string",null]},
          "pdRequired": false,
          "pdSource": { "from": "auth", "resolver": "self.db.validate_user" }
        },
        { "pdName": "search",
          "pdType": {"tag":"RefPrimitive","contents":["string",null]},
          "pdRequired": false
        }
      ]
    }
  }
}|]

irWithoutAuthMethods :: Value
irWithoutAuthMethods = [aesonQQ|{
  "irVersion": "2.0",
  "irBackend": "test",
  "irMethods": {
    "svc.ping": {
      "mdName": "ping",
      "mdFullPath": "svc.ping",
      "mdNamespace": "svc",
      "mdParams": [
        { "pdName": "message",
          "pdType": {"tag":"RefPrimitive","contents":["string",null]},
          "pdRequired": true
        }
      ]
    }
  }
}|]

irWithCookieButNoAuth :: Value
irWithCookieButNoAuth = [aesonQQ|{
  "irVersion": "2.0",
  "irBackend": "test",
  "irMethods": {
    "svc.origin_log": {
      "mdName": "origin_log",
      "mdFullPath": "svc.origin_log",
      "mdNamespace": "svc",
      "mdParams": [
        { "pdName": "origin",
          "pdType": {"tag":"RefPrimitive","contents":["string",null]},
          "pdRequired": false,
          "pdSource": { "from": "header", "key": "origin" }
        }
      ]
    }
  }
}|]

-- ─── 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)

  putStrLn "=== SAFE-6: backendRequiresCookieAuth detection ==="

  run $ boolCase "IR with #[from_auth] method → detected as requiring cookie auth"
    (backendRequiresCookieAuth irWithAuthMethod) True

  run $ boolCase "IR with only RPC params → not detected"
    (backendRequiresCookieAuth irWithoutAuthMethods) False

  run $ boolCase "IR with cookie/header params but no auth → not detected"
    (backendRequiresCookieAuth irWithCookieButNoAuth) False

  putStrLn "=== SAFE-6: detectCookieAuthMismatch end-to-end ==="

  Temp.withSystemTempDirectory "safe6" $ \tmp -> do
    let irFile   = tmp </> "ir.json"
        tsFile   = tmp </> "transport.ts"
    BS.writeFile irFile (BL.toStrict (Aeson.encode irWithAuthMethod))

    -- Case 1: auth required, no prior transport.ts → no warning
    r1 <- detectCookieAuthMismatch irFile tmp
    run $ case r1 of
      Nothing -> Pass "first build (no transport.ts) → no warning"
      Just _  -> Fail "first build" "expected Nothing; got warning"

    -- Case 2: auth required, transport.ts lacks SAFE-7 marker → warning
    TIO.writeFile tsFile "// legacy transport.ts, no marker\nexport const ws = null;"
    r2 <- detectCookieAuthMismatch irFile tmp
    run $ case r2 of
      Just w | "SAFE-6" `T.isInfixOf` w && "cookie" `T.isInfixOf` w ->
        Pass "stale transport.ts + auth required → warning mentions SAFE-6 + cookie"
      Just _  -> Fail "stale transport.ts" "got a warning but wrong content"
      Nothing -> Fail "stale transport.ts" "expected warning; got Nothing"

    -- Case 3: auth required, transport.ts contains SAFE-7 marker → no warning
    TIO.writeFile tsFile "// SAFE-7-cookie-auth-marker: ok\nexport const ws = null;"
    r3 <- detectCookieAuthMismatch irFile tmp
    run $ case r3 of
      Nothing -> Pass "SAFE-7-marked transport.ts → no warning"
      Just _  -> Fail "marked transport.ts" "expected Nothing; got warning"

    -- Case 4: no auth in backend → no warning regardless of transport.ts
    BS.writeFile irFile (BL.toStrict (Aeson.encode irWithoutAuthMethods))
    r4 <- detectCookieAuthMismatch irFile tmp  -- transport.ts still marked from case 3
    run $ case r4 of
      Nothing -> Pass "no-auth backend → no warning regardless of transport.ts"
      Just _  -> Fail "no-auth backend" "expected Nothing; got warning"

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

boolCase :: String -> Bool -> Bool -> TestResult
boolCase name got want
  | got == want = Pass name
  | otherwise   = Fail name ("expected " <> show want <> ", got " <> show got)