shomei-servant-0.2.0.0: test-openapi/Main.hs
{-# OPTIONS_GHC -Wno-missing-signatures -Wno-orphans #-}
-- | EP-27 M4 — OpenAPI 3.1 conformance for the served tree, 'Shomei.Servant.Api.ShomeiRoutes'.
--
-- Three layers:
--
-- 1. 'validateEveryToJSON' — for every JSON body type in the API, generate
-- arbitrary values and check their 'ToJSON' encoding validates against the
-- generated 'ToSchema'. This is what catches schema/JSON drift, including
-- the hand-written 'LoginResponse' @oneOf@ and the free-form 'Value' fields.
--
-- 2. Smoke assertions on the assembled 'shomeiOpenApi': the @openapi@ version
-- is @3.1.0@ and the document covers the expected number of paths.
--
-- 3. EP-3: the error surface. Every documented error code exists in the runtime
-- 'problemCatalog' at the documented status, so the spec cannot promise a code or a
-- status the server never sends; and the document 'Shomei.Servant.Error.problemBody'
-- actually writes for every catalog entry validates against the published @Problem@
-- schema, so the two halves of the envelope cannot drift apart. Plus the hygiene
-- invariants a generated client depends on: no @204@ carries content, no response
-- description is empty, every request body is required, and every authenticated
-- operation documents its @401@.
--
-- The 'Arbitrary' and 'Show' instances for the DTOs live here (orphans, test
-- only) so the production library carries no test dependency.
module Main (main) where
import Control.Monad (filterM)
import Data.Aeson (Result (..), ToJSON (..), Value (..), decode, eitherDecode, encode, fromJSON)
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KM
import Data.Char (isAsciiLower, isDigit)
import Data.Either (isLeft)
import Data.Foldable (toList)
import Data.Kind (Type)
import Data.List (nub, sort)
import Data.Maybe (isJust)
import Data.OpenApi (NamedSchema (..), Schema, ToSchema (..), validateJSON)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.IO qualified as TextIO
import Data.Type.Equality ((:~:) (Refl))
import GHC.TypeLits (Nat)
import Servant.API (NamedRoutes, NoContent (..), Verb, type (:>))
import Servant.API.MultiVerb (MultiVerb, Respond, RespondAs, WithHeaders)
import Servant.Health (ProbeResponses, ProbeStatus (..))
import Servant.OpenApi.Test (validateEveryToJSON)
import Servant.Server (ServerError (..))
import Shomei.Account.Admin.Api
import Shomei.Account.Api
import Shomei.Account.Dto
import Shomei.Account.User.Dto
import Shomei.Audit.Api
import Shomei.Audit.Dto
import Shomei.Authorization.Api
import Shomei.Mfa.Api
import Shomei.Mfa.Dto
import Shomei.OAuth.Api
import Shomei.Passkey.Api
import Shomei.Passkey.Dto
import Shomei.Servant.Api (OpenApiRoute, ShomeiRoutes)
import Shomei.Servant.Error (ProblemDetails (..), ProblemSpec (..), detailOccurrence, noProblemOccurrence, problemBody, problemCatalog, problemDetails, problemTypeFor, toProblemError)
import Shomei.Servant.OAuth (OAuthErrorResponse (..), TokenResponse (..))
import Shomei.Servant.OpenApi (shomeiOpenApi)
import Shomei.Session.Admin.Api
import Shomei.Session.Api
import Shomei.Session.Dto
import Shomei.SigningKey.Api
import System.Directory (doesFileExist)
import Test.Hspec
import Test.QuickCheck (Arbitrary (..), chooseInt, oneof)
import Test.QuickCheck.Instances ()
-- | @logout@ answers @204@ with @Set-Cookie@ headers. Servant models a header-carrying empty
-- response as a JSON-typed 'NoContent' body ('NoContentVerb' cannot carry headers), so
-- 'validateEveryToJSON' needs to generate and encode one. Test-only orphans; the wire response
-- is a genuine @204@ with no body.
instance Arbitrary NoContent where
arbitrary = pure NoContent
-- Encoded as an empty object so it validates against the empty schema below. Nothing is
-- serialized on the wire: a 204 carries no body, and servant renders 'NoContent' as "".
instance ToJSON NoContent where
toJSON NoContent = Object mempty
instance ToSchema NoContent where
declareNamedSchema _ = pure (NamedSchema (Just "NoContent") mempty)
instance Arbitrary ProbeStatus where
arbitrary = ProbeStatus <$> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary ProblemDetails where
arbitrary =
ProblemDetails
<$> pure "https://example.test/problems/example"
<*> arbitrary
<*> chooseInt (100, 599)
<*> arbitrary
<*> pure (Just "/requests/example")
<*> arbitrary
<*> arbitrary
instance Arbitrary OAuthErrorResponse where
arbitrary = OAuthErrorResponse <$> arbitrary <*> arbitrary
data OutcomeModel = SingleOutcome | MultiOutcome
type ResponseModel :: Type -> OutcomeModel
type family ResponseModel route where
ResponseModel (_ :> route) = ResponseModel route
ResponseModel (MultiVerb method content responses result) = 'MultiOutcome
ResponseModel (Verb method status content body) = 'SingleOutcome
type ResponseOwnsStatus :: Nat -> Type -> Bool
type family ResponseOwnsStatus status response where
ResponseOwnsStatus status (Respond status description body) = 'True
ResponseOwnsStatus status (Respond other description body) = 'False
ResponseOwnsStatus status (RespondAs content status description body) = 'True
ResponseOwnsStatus status (RespondAs content other description body) = 'False
ResponseOwnsStatus status (WithHeaders headers result response) = ResponseOwnsStatus status response
type ResponsesOwnStatus :: Nat -> [Type] -> Bool
type family ResponsesOwnStatus status responses where
ResponsesOwnStatus status '[] = 'False
ResponsesOwnStatus status (response ': responses) = Or (ResponseOwnsStatus status response) (ResponsesOwnStatus status responses)
type Or :: Bool -> Bool -> Bool
type family Or left right where
Or 'True right = 'True
Or 'False right = right
type OperationOwnsStatus :: Nat -> Type -> Bool
type family OperationOwnsStatus status route where
OperationOwnsStatus status (_ :> route) = OperationOwnsStatus status route
OperationOwnsStatus status (MultiVerb method content responses result) = ResponsesOwnStatus status responses
type Classified route =
( ResponseModel route :~: 'MultiOutcome,
OperationOwnsStatus 503 route :~: 'True
)
accountWitnesses =
( (Refl, Refl) :: Classified SignupRoute,
(Refl, Refl) :: Classified VerifyEmailRequestRoute,
(Refl, Refl) :: Classified VerifyEmailConfirmRoute,
(Refl, Refl) :: Classified PasswordResetRequestRoute,
(Refl, Refl) :: Classified PasswordResetConfirmRoute,
(Refl, Refl) :: Classified PasswordChangeRoute,
(Refl, Refl) :: Classified MeRoute,
(Refl, Refl) :: Classified ListUsersRoute,
(Refl, Refl) :: Classified GetUserRoute,
(Refl, Refl) :: Classified SuspendUserRoute,
(Refl, Refl) :: Classified ReinstateUserRoute,
(Refl, Refl) :: Classified DeleteUserRoute,
(Refl, Refl) :: Classified AdminPasswordResetRoute
)
sessionWitnesses =
( (Refl, Refl) :: Classified LoginRoute,
(Refl, Refl) :: Classified RefreshRoute,
(Refl, Refl) :: Classified LogoutRoute,
(Refl, Refl) :: Classified CurrentSessionRoute,
(Refl, Refl) :: Classified ListSessionsRoute,
(Refl, Refl) :: Classified RevokeSessionsRoute,
(Refl, Refl) :: Classified RevokeSessionRoute
)
passkeyWitnesses =
( (Refl, Refl) :: Classified RegisterBeginRoute,
(Refl, Refl) :: Classified RegisterCompleteRoute,
(Refl, Refl) :: Classified ListPasskeysRoute,
(Refl, Refl) :: Classified RemovePasskeyRoute,
(Refl, Refl) :: Classified PasskeyLoginBeginRoute,
(Refl, Refl) :: Classified PasskeyLoginCompleteRoute
)
mfaWitnesses =
( (Refl, Refl) :: Classified MfaCompleteRoute,
(Refl, Refl) :: Classified TotpEnrollRoute,
(Refl, Refl) :: Classified TotpVerifyRoute,
(Refl, Refl) :: Classified TotpDeleteRoute,
(Refl, Refl) :: Classified RecoveryCodesGenerateRoute,
(Refl, Refl) :: Classified RecoveryCodesCountRoute
)
otherMultiWitnesses =
( (Refl, Refl) :: Classified AuditEventsRoute,
(Refl, Refl) :: Classified GrantRoleRoute,
(Refl, Refl) :: Classified RevokeRoleRoute,
(Refl, Refl) :: Classified AuthorizeRoute,
(Refl, Refl) :: Classified TokenRoute,
(Refl, Refl) :: Classified UserinfoRoute,
(Refl, Refl) :: Classified IntrospectRoute,
(Refl, Refl) :: Classified RevokeRoute,
(Refl, Refl) :: Classified OidcDiscoveryRoute
)
ordinaryWitnesses =
( Refl :: ResponseModel JwksRoute :~: 'SingleOutcome,
Refl :: ResponseModel OpenApiRoute :~: 'SingleOutcome
)
health503Witness :: ResponsesOwnStatus 503 ProbeResponses :~: 'True
health503Witness = Refl
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
problemCatalogDocument <- runIO do
let candidates = ["docs/user/problem-details.md", "../docs/user/problem-details.md"]
existing <- filterM doesFileExist candidates
case existing of
path : _ -> TextIO.readFile path
[] -> fail "docs/user/problem-details.md not found"
describe "OpenAPI 3.1 schema: ToJSON matches ToSchema" $
validateEveryToJSON (Proxy :: Proxy (NamedRoutes ShomeiRoutes))
describe "strict authentication request decoding" $ do
it "requires methods on the MFA login-response arm" $
(eitherDecode "{\"status\":\"mfa_required\",\"ceremonyId\":\"c\",\"options\":{}}" :: Either String LoginResponse)
`shouldSatisfy` isLeft
it "rejects the removed flat MFA completion shape" $
(eitherDecode "{\"ceremonyId\":\"c\",\"totpCode\":\"123456\"}" :: Either String MfaCompleteRequest)
`shouldSatisfy` isLeft
it "rejects extra proof arms in a tagged MFA proof" $
(eitherDecode "{\"type\":\"totp\",\"code\":\"123456\",\"assertion\":{}}" :: Either String MfaProof)
`shouldSatisfy` isLeft
describe "shomeiOpenApi document" $ do
it "declares OpenAPI version 3.1.0" $
lookupTop "openapi" `shouldBe` Just (String "3.1.0")
it "covers exactly 41 paths" $
pathCount `shouldBe` 41
it "covers the exact served method and path inventory" $
sort (map fst operations) `shouldBe` expectedOperations
-- 'ResponseModel' has no 'MultiVerb1' equation. Replacing any named route witness below
-- with MultiVerb1 therefore makes this module fail to compile rather than silently pass.
it "rejects MultiVerb1, keeps the exact ordinary allow-list, and gives every other JSON route an operation-owned 503" $
accountWitnesses `seq`
sessionWitnesses `seq`
passkeyWitnesses `seq`
mfaWitnesses `seq`
otherMultiWitnesses `seq`
ordinaryWitnesses `seq`
health503Witness `seq`
True `shouldBe` True
describe "EP-4: /oauth/token speaks RFC 6749 behind a problem-details edge throttle" $ do
it "declares the OAuthErrorResponse schema" $
(lookupTop "components" >>= field "schemas" >>= field "OAuthErrorResponse") `shouldSatisfy` isJust
-- Handler-owned failures stay in the shape a stock OAuth2 client parses. The sole exception
-- is 429: the edge limiter answers before routing and therefore uses the shared problem
-- document (with Retry-After), exactly as the runtime middleware does.
it "documents only the edge 429 as problem+json on /oauth/token" $
[ Key.toText status
| (path, Object item) <- KM.toList paths,
path == "/oauth/token",
(_, Object op) <- KM.toList item,
(status, resp) <- responsesOf op,
isProblemResponse resp
]
`shouldBe` ["429"]
it "documents the protocol-owned error statuses" $
responseStatusesAt "/oauth/token" `shouldBe` ["200", "400", "401", "404", "429", "500", "503"]
describe "EP-5: the OIDC discovery document is on the OAuth side of the envelope boundary" $ do
-- Reached by OIDC tooling, so its "provider disabled" refusal must be a shape that tooling
-- parses. The OIDC route's protocol response list is the sole source of the alternative.
it "documents no problem+json response on /.well-known/openid-configuration" $
[ Key.toText status
| (path, Object item) <- KM.toList paths,
path == "/.well-known/openid-configuration",
(_, Object op) <- KM.toList item,
(status, resp) <- responsesOf op,
isProblemResponse resp
]
`shouldBe` []
it "documents the 404 it answers when the provider is disabled" $
"404" `shouldSatisfy` (`elem` responseStatusesAt "/.well-known/openid-configuration")
describe "EP-5: /oauth/userinfo authenticates inside the OAuth envelope" $ do
it "requires bearer authentication" $
(requiresBearer <$> lookup "get /oauth/userinfo" operations) `shouldBe` Just True
it "documents only OAuth JSON failures, never application problems" $
[ Key.toText status
| (path, Object item) <- KM.toList paths,
path == "/oauth/userinfo",
(_, Object op) <- KM.toList item,
(status, resp) <- responsesOf op,
isProblemResponse resp
]
`shouldBe` []
describe "EP-5: /oauth/authorize speaks RFC 6749, and only its no-redirect failures are statuses" $ do
it "documents no problem+json response on /oauth/authorize" $
[ Key.toText status
| (path, Object item) <- KM.toList paths,
path == "/oauth/authorize",
(_, Object op) <- KM.toList item,
(status, resp) <- responsesOf op,
isProblemResponse resp
]
`shouldBe` []
-- Every OTHER authorize failure -- bad response_type, PKCE policy, disallowed scope -- is a
-- 302 back to the validated redirect_uri, so it is not a status this operation declares.
it "documents only protocol statuses rather than redirect query error values" $
responseStatusesAt "/oauth/authorize" `shouldBe` ["302", "400", "401", "404", "500", "503"]
describe "EP-3: the error surface cannot drift from the runtime catalog" $ do
it "declares the ProblemDetails schema with the RFC 9457 profile members" $
problemRequired `shouldBe` ["code", "retryable", "status", "title", "type"]
-- The published schema and the bytes the server writes come from different code
-- (`problemSchema` in OpenApi.hs, `problemBody` in Error.hs). Validate the real runtime
-- document of every catalog entry, with and without a `detail`, against the schema as it
-- appears in the serialized document — the artifact a client generator actually reads.
it "validates the real runtime document of every catalog entry against the published Problem schema" $
[ (problemCode p, isJust detail, errs)
| p <- problemCatalog,
detail <- [Nothing, Just "a request-specific explanation"],
let occurrence = maybe noProblemOccurrence detailOccurrence detail,
let errs = validateJSON mempty publishedProblemSchema (problemBody p occurrence),
not (null errs)
]
`shouldBe` []
it "keeps body status, type, code, title, and retryability synchronized" $
[ p.problemCode
| p <- problemCatalog,
let body = problemDetails p noProblemOccurrence,
body.status /= errHTTPCode p.problemStatus
|| body.problemType /= problemTypeFor p.problemCode
|| body.code /= p.problemCode
|| body.title /= p.problemTitle
|| body.retryable /= p.problemRetryable
]
`shouldBe` []
it "uses a URI-safe code alphabet and a one-to-one type/code mapping" $ do
[p.problemCode | p <- problemCatalog, Text.any (\c -> not (isAsciiLower c || isDigit c || c == '_')) p.problemCode] `shouldBe` []
length (nub (map (problemTypeFor . problemCode) problemCatalog))
`shouldBe` length (nub (map problemCode problemCatalog))
it "documents an explicit public anchor for every code" $
[p.problemCode | p <- problemCatalog, not (Text.isInfixOf ("id=\"" <> p.problemCode <> "\"") problemCatalogDocument)]
`shouldBe` []
it "renders matching RFC 9457 bodies and media types at the pre-handler boundary" $
[ p.problemCode
| p <- problemCatalog,
let rendered = toProblemError p noProblemOccurrence,
lookup "Content-Type" rendered.errHeaders /= Just "application/problem+json"
|| (decode rendered.errBody :: Maybe ProblemDetails) /= Just (problemDetails p noProblemOccurrence)
]
`shouldBe` []
it "decodes unknown RFC 9457 extension members" $
(eitherDecode "{\"type\":\"https://example.test/problem\",\"title\":\"Example\",\"status\":400,\"code\":\"example\",\"retryable\":false,\"future\":true}" :: Either String ProblemDetails)
`shouldSatisfy` either (const False) (const True)
it "documents a 401 on every operation that requires a bearer token" $
[key | (key, op) <- operations, requiresBearer op, not (declares "401" op)] `shouldBe` []
describe "EP-3: spec hygiene a generated client depends on" $ do
it "puts no content on a 204" $
[key | (key, op) <- operations, responseHasContent "204" op] `shouldBe` []
it "gives every response a non-empty description" $
[key <> " " <> status | (key, op) <- operations, status <- emptyDescriptions op] `shouldBe` []
it "marks every request body required" $
[key | (key, op) <- operations, Just body <- [KM.lookup "requestBody" op], not (isRequired body)] `shouldBe` []
where
decoded :: KM.KeyMap Value
decoded = case decode (encode shomeiOpenApi) of
Just (Object o) -> o
_ -> error "shomeiOpenApi did not encode to a JSON object"
lookupTop k = KM.lookup k decoded
paths = case lookupTop "paths" of
Just (Object ps) -> ps
_ -> error "shomeiOpenApi has no paths object"
pathCount = KM.size paths
problemSchemaJson = case lookupTop "components" >>= field "schemas" >>= field "ProblemDetails" of
Just v -> v
Nothing -> error "shomeiOpenApi has no components.schemas.ProblemDetails"
-- Round-tripped through the serialized document on purpose: this is the schema a client
-- generator reads, not the Haskell value that produced it.
publishedProblemSchema :: Schema
publishedProblemSchema = case fromJSON problemSchemaJson of
Success s -> s
Error e -> error ("components.schemas.Problem does not decode as a Schema: " <> e)
problemRequired = case field "required" problemSchemaJson of
Just (Array xs) -> sort [t | String t <- toList xs]
_ -> error "shomeiOpenApi has no components.schemas.Problem.required"
-- Every (method, path) operation object in the document, labelled for failure messages.
operations :: [(Text, KM.KeyMap Value)]
operations =
[ (Key.toText method <> " " <> Key.toText path, op)
| (path, Object item) <- KM.toList paths,
(method, Object op) <- KM.toList item,
method `elem` operationMethods
]
operationMethods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
expectedOperations =
sort
[ "delete /v1/admin/sessions/{sessionId}",
"delete /v1/admin/users/{userId}",
"delete /v1/admin/users/{userId}/roles/{role}",
"delete /v1/admin/users/{userId}/sessions",
"delete /v1/auth/passkeys/{passkeyId}",
"delete /v1/auth/totp",
"get /.well-known/jwks.json",
"get /.well-known/openid-configuration",
"get /health/live",
"get /health/ready",
"get /oauth/authorize",
"get /oauth/userinfo",
"get /openapi.json",
"get /v1/admin/audit/events",
"get /v1/admin/users",
"get /v1/admin/users/{userId}",
"get /v1/admin/users/{userId}/sessions",
"get /v1/auth/me",
"get /v1/auth/passkeys",
"get /v1/auth/recovery-codes",
"get /v1/auth/session",
"post /oauth/introspect",
"post /oauth/revoke",
"post /oauth/token",
"post /v1/admin/users/{userId}/password-reset",
"post /v1/admin/users/{userId}/reinstate",
"post /v1/admin/users/{userId}/suspend",
"post /v1/auth/login",
"post /v1/auth/login/passkey/begin",
"post /v1/auth/login/passkey/complete",
"post /v1/auth/logout",
"post /v1/auth/mfa/complete",
"post /v1/auth/passkeys/register/begin",
"post /v1/auth/passkeys/register/complete",
"post /v1/auth/password-reset/confirm",
"post /v1/auth/password-reset/request",
"post /v1/auth/password/change",
"post /v1/auth/recovery-codes",
"post /v1/auth/refresh",
"post /v1/auth/signup",
"post /v1/auth/totp/enroll",
"post /v1/auth/totp/verify",
"post /v1/auth/verify-email/confirm",
"post /v1/auth/verify-email/request",
"put /v1/admin/users/{userId}/roles/{role}"
]
responsesOf op = case KM.lookup "responses" op of
Just (Object rs) -> [(status, r) | (status, Object r) <- KM.toList rs]
_ -> []
isProblemResponse resp = KM.member "application/problem+json" (contentOf resp)
responseStatusesAt wanted =
sort
[ Key.toText status
| (path, Object item) <- KM.toList paths,
path == wanted,
(_, Object op) <- KM.toList item,
(status, _) <- responsesOf op
]
contentOf resp = case KM.lookup "content" resp of
Just (Object c) -> c
_ -> KM.empty
requiresBearer op = case KM.lookup "security" op of
Just (Array xs) -> not (null xs)
_ -> False
declares status op = any ((== Key.fromText status) . fst) (responsesOf op)
responseHasContent status op =
or [KM.member "content" r | (s, r) <- responsesOf op, s == Key.fromText status]
emptyDescriptions op =
[ Key.toText status
| (status, r) <- responsesOf op,
KM.lookup "description" r `elem` [Nothing, Just (String "")]
]
isRequired body = field "required" body == Just (Bool True)
field :: Text -> Value -> Maybe Value
field k = \case
Object o -> KM.lookup (Key.fromText k) o
_ -> Nothing
-- ---------------------------------------------------------------------------
-- Show instances (needed by validateEveryToJSON for counterexamples)
-- ---------------------------------------------------------------------------
deriving stock instance Show SignupRequest
deriving stock instance Show SignupResponse
deriving stock instance Show LoginRequest
deriving stock instance Show LoginResponse
deriving stock instance Show RefreshRequest
deriving stock instance Show VerifyEmailRequest
deriving stock instance Show ConfirmEmailVerificationRequest
deriving stock instance Show PasswordResetRequest
deriving stock instance Show ConfirmPasswordResetRequest
deriving stock instance Show ChangePasswordRequest
deriving stock instance Show TokenPairResponse
deriving stock instance Show UserResponse
deriving stock instance Show SessionResponse
deriving stock instance Show AdminUserResponse
deriving stock instance Show AdminUsersPage
deriving stock instance Show MfaCompleteRequest
deriving stock instance Show MfaProof
deriving stock instance Show TotpEnrollResponse
deriving stock instance Show TotpVerifyRequest
deriving stock instance Show TotpRemoveRequest
deriving stock instance Show RecoveryCodesResponse
deriving stock instance Show RecoveryCodesCountResponse
deriving stock instance Show PasskeyRegisterBeginResponse
deriving stock instance Show PasskeyRegisterCompleteRequest
deriving stock instance Show PasskeyResponse
deriving stock instance Show PasskeyLoginBeginResponse
deriving stock instance Show PasskeyLoginCompleteRequest
deriving stock instance Show AuditEventResponse
deriving stock instance Show AuditEventsPage
-- ---------------------------------------------------------------------------
-- Arbitrary instances (Text/Value come from quickcheck-instances)
-- ---------------------------------------------------------------------------
instance Arbitrary SignupRequest where
arbitrary = SignupRequest <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary UserResponse where
arbitrary = UserResponse <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary TokenPairResponse where
arbitrary = TokenPairResponse <$> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary SignupResponse where
arbitrary = SignupResponse <$> arbitrary <*> arbitrary
instance Arbitrary LoginRequest where
arbitrary = LoginRequest <$> arbitrary <*> arbitrary
instance Arbitrary LoginResponse where
arbitrary =
oneof
[ LoginCompleteResponse <$> arbitrary <*> arbitrary,
LoginMfaRequiredResponse <$> arbitrary <*> arbitrary <*> arbitrary
]
instance Arbitrary RefreshRequest where
arbitrary = RefreshRequest <$> arbitrary
instance Arbitrary VerifyEmailRequest where
arbitrary = VerifyEmailRequest <$> arbitrary
instance Arbitrary ConfirmEmailVerificationRequest where
arbitrary = ConfirmEmailVerificationRequest <$> arbitrary
instance Arbitrary PasswordResetRequest where
arbitrary = PasswordResetRequest <$> arbitrary
instance Arbitrary ConfirmPasswordResetRequest where
arbitrary = ConfirmPasswordResetRequest <$> arbitrary <*> arbitrary
instance Arbitrary ChangePasswordRequest where
arbitrary = ChangePasswordRequest <$> arbitrary <*> arbitrary
instance Arbitrary MfaCompleteRequest where
arbitrary = MfaCompleteRequest <$> arbitrary <*> arbitrary
instance Arbitrary MfaProof where
arbitrary = oneof [PasskeyProof <$> arbitrary, TotpProof <$> arbitrary, RecoveryCodeProof <$> arbitrary]
instance Arbitrary TotpEnrollResponse where
arbitrary = TotpEnrollResponse <$> arbitrary <*> arbitrary
instance Arbitrary TotpVerifyRequest where
arbitrary = TotpVerifyRequest <$> arbitrary
instance Arbitrary TotpRemoveRequest where
arbitrary = TotpRemoveRequest <$> arbitrary <*> arbitrary
instance Arbitrary RecoveryCodesResponse where
arbitrary = RecoveryCodesResponse <$> arbitrary
instance Arbitrary RecoveryCodesCountResponse where
arbitrary = RecoveryCodesCountResponse <$> arbitrary
instance Arbitrary PasskeyRegisterBeginResponse where
arbitrary = PasskeyRegisterBeginResponse <$> arbitrary <*> arbitrary
instance Arbitrary PasskeyRegisterCompleteRequest where
arbitrary = PasskeyRegisterCompleteRequest <$> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary PasskeyResponse where
arbitrary = PasskeyResponse <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary PasskeyLoginBeginResponse where
arbitrary = PasskeyLoginBeginResponse <$> arbitrary <*> arbitrary
instance Arbitrary PasskeyLoginCompleteRequest where
arbitrary = PasskeyLoginCompleteRequest <$> arbitrary <*> arbitrary
instance Arbitrary TokenResponse where
arbitrary = TokenResponse <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary SessionResponse where
arbitrary = SessionResponse <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary AdminUserResponse where
arbitrary = AdminUserResponse <$> arbitrary <*> arbitrary
instance Arbitrary AdminUsersPage where
arbitrary = AdminUsersPage <$> arbitrary <*> arbitrary
instance Arbitrary AuditEventResponse where
arbitrary =
AuditEventResponse <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary AuditEventsPage where
arbitrary = AuditEventsPage <$> arbitrary <*> arbitrary