packages feed

polysemy-account-api-0.2.0.0: test/Polysemy/Account/Api/Test/Request.hs

module Polysemy.Account.Api.Test.Request where

import qualified Data.CaseInsensitive as CaseInsensitive
import Exon.Quote (exon)
import Network.Wai (Request, requestHeaders, requestMethod)
import Network.Wai.Test (SRequest (SRequest), SResponse, Session, defaultRequest, runSession, setPath, srequest)
import Polysemy.Db (interpretIdNumFrom)
import Polysemy.Test (TestError (TestError), UnitTest)
import Prelude hiding (get, put)
import Servant (
  Context (EmptyContext, (:.)),
  DefaultErrorFormatters,
  ErrorFormatters,
  HasContextEntry,
  HasServer,
  ServerError,
  ServerT,
  type (.++),
  )
import Servant.Auth.Server (IsSecure (NotSecure), cookieIsSecure, defaultCookieSettings)
import Sqel (Uid)
import Zeugma (TestStack, resumeTest, runTestFrozen)

import Polysemy.Account.Accounts (register)
import Polysemy.Account.Api.Effect.Authorize (Authorize)
import qualified Polysemy.Account.Api.Effect.Jwt as Jwt
import Polysemy.Account.Api.Effect.Jwt (Jwt)
import Polysemy.Account.Api.Interpreter.Authorize (interpretAuthorizeP)
import Polysemy.Account.Api.Interpreter.Jwt (interpretJwt)
import Polysemy.Account.Api.Native (AuthContext)
import Polysemy.Account.Api.NativeContext (runServerSem)
import Polysemy.Account.Api.Test.Data.Request (Headers, Method, methodUpper)
import qualified Polysemy.Account.Api.Test.Effect.TestClient as TestClient
import Polysemy.Account.Api.Test.Effect.TestClient (TestClient)
import Polysemy.Account.Data.Account (Account)
import Polysemy.Account.Data.AccountAuth (AccountAuth)
import Polysemy.Account.Data.AccountCredentials (AccountCredentials (AccountCredentials))
import qualified Polysemy.Account.Data.AccountStatus as AccountStatus
import Polysemy.Account.Data.AccountsError (AccountsError)
import Polysemy.Account.Data.AuthToken (AuthToken (AuthToken))
import Polysemy.Account.Data.AuthedAccount (AuthedAccount (AuthedAccount))
import Polysemy.Account.Data.Privilege (Privilege (Admin, Web))
import Polysemy.Account.Data.RawPassword (rawPassword)
import qualified Polysemy.Account.Effect.Accounts as Accounts
import Polysemy.Account.Effect.Accounts (Accounts)
import Polysemy.Account.Interpreter.Accounts (interpretAccountsState)

type ServerCtx (api :: Type) context =
  (
    HasServer api context,
    HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters
  )

type Server (api :: Type) context r =
  (
    Members [Log, Error Text, Embed IO, Final IO] r,
    ServerCtx api context
  )

type TestServer (api :: Type) r =
  ServerT api (Sem (Stop ServerError : r))

req :: Text -> Headers -> Request
req path headers =
  setPath defaultRequest { requestHeaders = (first CaseInsensitive.mk <$> headers) } . encodeUtf8 $ path

runSessionSem ::
  ∀ (api :: Type) context r a .
  Server api context r =>
  TestServer api r ->
  Context context ->
  Session a ->
  Sem r a
runSessionSem srv context =
  runServerSem @api srv context . runSession

runSessionSemJwtCtx ::
  ∀ (api :: Type) ctx i p r a .
  Member (Jwt (AuthedAccount i p) !! ()) r =>
  Server api (AuthContext ++ ctx) r =>
  Context ctx ->
  TestServer api r ->
  Session a ->
  Sem r a
runSessionSemJwtCtx ctx srv session = do
  jwtSettings <- Jwt.settings @(AuthedAccount i p) !>> throw "jwt"
  runSessionSem @api @(AuthContext ++ ctx) srv (context jwtSettings) session
  where
    context jwtSettings =
      jwtSettings :. defaultCookieSettings { cookieIsSecure = NotSecure } :. ctx

runSessionSemJwt ::
  ∀ (api :: Type) i p r a .
  Member (Jwt (AuthedAccount i p) !! ()) r =>
  Server api AuthContext r =>
  TestServer api r ->
  Session a ->
  Sem r a
runSessionSemJwt =
  runSessionSemJwtCtx @api EmptyContext

jsonType :: (ByteString, ByteString)
jsonType =
  ("content-type", "application/json")

authHeader :: (ByteString, ByteString)
authHeader =
  ("authorization", "Basic QWxhZGRpbjpPcGVuU2VzYW1l")

reqJson :: Method -> Text -> Headers -> LByteString -> SRequest
reqJson method path headers body =
  SRequest (req path (jsonType : headers)) { requestMethod = methodUpper method } body

defaultAccount :: AuthedAccount Int [Privilege]
defaultAccount =
  AuthedAccount 1 1 "user" AccountStatus.Active [Web]

run ::
  ∀ (api :: Type) ctx r .
  Member (Jwt (AuthedAccount Int [Privilege]) !! ()) r =>
  Server api (AuthContext ++ ctx) r =>
  Context ctx ->
  TestServer api r ->
  Method ->
  Text ->
  Headers ->
  LByteString ->
  Sem r SResponse
run ctx srv method path headers body =
  runSessionSemJwtCtx @api ctx srv (srequest (reqJson method path headers body))

makeAccount ::
  Members [Accounts Int [Privilege] !! AccountsError, Jwt (AuthedAccount Int [Privilege]) !! (), Error TestError] r =>
  AccountCredentials ->
  [Privilege] ->
  Sem r (Text, (ByteString, ByteString))
makeAccount creds privs =
  resumeTest @AccountsError do
    root <- register creds
    let i = root ^. #id
    Accounts.setStatus i AccountStatus.Active
    Accounts.updatePrivileges i (const privs)
    AuthToken token <- Jwt.makeToken root !>> throw (TestError "jwt")
    pure (token, ("authorization", [exon|Bearer #{encodeUtf8 token}|]))

interpretTestClientCtx ::
  ∀ (api :: Type) ctx r .
  Members [Accounts Int [Privilege] !! AccountsError, Jwt (AuthedAccount Int [Privilege]) !! (), Error TestError] r =>
  Server api (AuthContext ++ ctx) r =>
  Context ctx ->
  TestServer api r ->
  InterpreterFor TestClient r
interpretTestClientCtx ctx server =
  interpret \case
    TestClient.Request method path headers body ->
      run @api ctx server method path headers body
    TestClient.MakeAdmin ->
      makeAccount (AccountCredentials "root" (rawPassword "root")) [Admin]
    TestClient.MakeUser ->
      makeAccount (AccountCredentials "user" (rawPassword "user")) [Web]

interpretTestClient ::
  ∀ (api :: Type) r .
  Members [Accounts Int [Privilege] !! AccountsError, Jwt (AuthedAccount Int [Privilege]) !! (), Error TestError] r =>
  Server api AuthContext r =>
  TestServer api r ->
  InterpreterFor TestClient r
interpretTestClient =
  interpretTestClientCtx @api EmptyContext

type TestEffects =
  [
    Authorize Int [Privilege] [Privilege],
    Accounts Int [Privilege] !! AccountsError,
    Stop ServerError,
    Jwt (AuthedAccount Int [Privilege]) !! (),
    Error Text
  ]

interpretAccounts ::
  Members [Log, Error TestError, Embed IO] r =>
  [Uid Int (Account [Privilege])] ->
  [Uid Int (AccountAuth Int)] ->
  InterpreterFor (Accounts Int [Privilege] !! AccountsError) r
interpretAccounts accounts auths =
  mapError TestError .
  interpretIdNumFrom 3 .
  interpretAccountsState def accounts auths .
  raiseUnder3

runServer ::
  Members [Error TestError, Stop Text, Log, Resource, Async, Race, Embed IO] r =>
  [Uid Int (Account [Privilege])] ->
  [Uid Int (AccountAuth Int)] ->
  InterpretersFor TestEffects r
runServer accounts auths =
  mapError TestError .
  raiseResumable interpretJwt .
  showStop @ServerError .
  interpretAccounts accounts auths .
  interpretAuthorizeP

runApiTestCtx ::
  ∀ (api :: Type) ctx .
  ServerCtx api (AuthContext ++ ctx) =>
  Context ctx ->
  [Uid Int (Account [Privilege])] ->
  [Uid Int (AccountAuth Int)] ->
  TestServer api (TestEffects ++ TestStack) ->
  Sem (TestClient : TestEffects ++ TestStack) () ->
  UnitTest
runApiTestCtx ctx accounts auths server =
  runTestFrozen .
  runServer accounts auths .
  interpretTestClientCtx @api ctx server

runApiTest ::
  ∀ (api :: Type) .
  ServerCtx api AuthContext =>
  [Uid Int (Account [Privilege])] ->
  [Uid Int (AccountAuth Int)] ->
  TestServer api (TestEffects ++ TestStack) ->
  Sem (TestClient : TestEffects ++ TestStack) () ->
  UnitTest
runApiTest =
  runApiTestCtx @api EmptyContext