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