packages feed

cachix-api 1.13.0 → 1.14.0

raw patch · 20 files changed

+168/−49 lines, 20 filesdep +openapi3dep ~websocketsPVP ok

version bump matches the API change (PVP)

Dependencies added: openapi3

Dependency ranges changed: websockets

API changes (from Hackage documentation)

+ Cachix.API.WebSocket.Client: HandshakeRejected :: ResponseHead -> HandshakeRejected
+ Cachix.API.WebSocket.Client: [rejectionResponse] :: HandshakeRejected -> ResponseHead
+ Cachix.API.WebSocket.Client: instance GHC.Exception.Type.Exception Cachix.API.WebSocket.Client.HandshakeRejected
+ Cachix.API.WebSocket.Client: instance GHC.Show.Show Cachix.API.WebSocket.Client.HandshakeRejected
+ Cachix.API.WebSocket.Client: newtype HandshakeRejected
+ Cachix.API.WebSocket.Client: withHandshakeErrors :: IO a -> IO a
+ Cachix.API.WebSocketSubprotocol: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.API.WebSocketSubprotocol.Log
+ Cachix.Types.BinaryCache: instance Data.OpenApi.Internal.ParamSchema.ToParamSchema Cachix.Types.BinaryCache.CompressionMethod
+ Cachix.Types.BinaryCache: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.BinaryCache.BinaryCache
+ Cachix.Types.BinaryCache: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.BinaryCache.CompressionMethod
+ Cachix.Types.ByteStringStreaming: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.ByteStringStreaming.ByteStringStreaming
+ Cachix.Types.ByteStringStreaming: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.ByteStringStreaming.LazyByteStringStreaming
+ Cachix.Types.Deploy: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.Deploy.Deploy
+ Cachix.Types.DeployResponse.V1: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.DeployResponse.V1.DeployResponse
+ Cachix.Types.DeployResponse.V2: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.DeployResponse.V2.DeployResponse
+ Cachix.Types.DeployResponse.V2: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.DeployResponse.V2.Details
+ Cachix.Types.Deployment: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.Deployment.Deployment
+ Cachix.Types.Deployment: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.Deployment.Status
+ Cachix.Types.MultipartUpload: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.MultipartUpload.CompletedMultipartUpload
+ Cachix.Types.MultipartUpload: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.MultipartUpload.CompletedPart
+ Cachix.Types.MultipartUpload: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.MultipartUpload.CreateMultipartUploadResponse
+ Cachix.Types.MultipartUpload: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.MultipartUpload.SigningData
+ Cachix.Types.MultipartUpload: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.MultipartUpload.UploadPartResponse
+ Cachix.Types.NarFileName: instance Data.OpenApi.Internal.ParamSchema.ToParamSchema Cachix.Types.NarFileName.NarFileName
+ Cachix.Types.NarInfo: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.NarInfo.CachixNarInfo
+ Cachix.Types.NarInfoCreate: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.NarInfoCreate.NarInfoCreate
+ Cachix.Types.NarInfoHash: instance Data.OpenApi.Internal.ParamSchema.ToParamSchema Cachix.Types.NarInfoHash.NarInfoHash
+ Cachix.Types.NarInfoHash: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.NarInfoHash.NarInfoHash
+ Cachix.Types.NixCacheInfo: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.NixCacheInfo.NixCacheInfo
+ Cachix.Types.Permission: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.Permission.Permission
+ Cachix.Types.PinCreate: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.PinCreate.Keep
+ Cachix.Types.PinCreate: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.PinCreate.PinCreate
+ Cachix.Types.SigningKeyCreate: instance Data.OpenApi.Internal.Schema.ToSchema Cachix.Types.SigningKeyCreate.SigningKeyCreate

Files

CHANGELOG.md view
@@ -7,6 +7,12 @@  ## [Unreleased] +## [1.14.0] - 2026-11-05++### Added++- Add OpenAPI 3.0 schemas for API payloads, streaming responses, and URL parameters.+ ## [0.7.0] - 2022-01-12  - Support Cachix Deploy
cachix-api.cabal view
@@ -1,6 +1,6 @@ cabal-version:   2.2 name:            cachix-api-version:         1.13.0+version:         1.14.0 synopsis:        Servant HTTP API specification for https://cachix.org homepage:        https://github.com/cachix/cachix#readme bug-reports:     https://github.com/cachix/cachix/issues@@ -72,6 +72,7 @@     Cachix.API.Deploy.V2     Cachix.API.Error     Cachix.API.Signing+    Cachix.API.WebSocket.Client     Cachix.API.WebSocketSubprotocol     Cachix.Types.BinaryCache     Cachix.Types.ByteStringStreaming@@ -104,6 +105,7 @@     , http-media     , jose     , nix-narinfo+    , openapi3     , protolude     , resourcet     , safe-exceptions@@ -115,7 +117,7 @@     , time     , unordered-containers     , uuid-    , websockets+    , websockets            >=0.13    if flag(ram)     build-depends:@@ -134,6 +136,7 @@   other-modules:     DeploySpec     Spec+    WebSocketClientSpec    hs-source-dirs:     test   build-depends:@@ -142,5 +145,6 @@     , cachix-api     , hspec     , protolude+    , websockets    build-tool-depends: hspec-discover:hspec-discover
+ src/Cachix/API/WebSocket/Client.hs view
@@ -0,0 +1,26 @@+-- | WebSocket clients with explicit HTTP handshake rejections.+module Cachix.API.WebSocket.Client+  ( HandshakeRejected (..),+    withHandshakeErrors,+  )+where++import qualified Network.WebSockets as WS+import Protolude++newtype HandshakeRejected = HandshakeRejected+  { rejectionResponse :: WS.ResponseHead+  }+  deriving (Show)++instance Exception HandshakeRejected++-- | Normalize HTTP handshake rejections thrown by the supplied action.+-- Invalid HTTP 101 upgrade responses retain their original exception.+withHandshakeErrors :: IO a -> IO a+withHandshakeErrors action = action `catch` handleHandshakeError+  where+    handleHandshakeError (WS.RequestRejected _ response) = throwIO $ HandshakeRejected response+    handleHandshakeError (WS.MalformedResponse response _)+      | WS.responseCode response /= 101 = throwIO $ HandshakeRejected response+    handleHandshakeError exception = throwIO exception
src/Cachix/API/WebSocketSubprotocol.hs view
@@ -6,6 +6,7 @@ import qualified Control.Concurrent.STM.TMQueue as TMQueue import qualified Control.Exception.Safe as Safe import qualified Data.Aeson as Aeson+import qualified Data.OpenApi as OpenApi import Data.Time (UTCTime) import Data.UUID (UUID) import qualified Network.WebSockets as WS@@ -100,4 +101,4 @@   { line :: Text,     time :: UTCTime   }-  deriving (Generic, Show, Aeson.ToJSON, Aeson.FromJSON)+  deriving (Generic, Show, Aeson.ToJSON, Aeson.FromJSON, OpenApi.ToSchema)
src/Cachix/Types/BinaryCache.hs view
@@ -2,7 +2,8 @@  import Cachix.Types.Permission (Permission) import Data.Aeson (FromJSON, ToJSON)-import Data.Swagger (ToParamSchema (..), ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Protolude import Servant.API import Text.Read@@ -18,10 +19,10 @@     permission :: Permission,     preferredCompressionMethod :: CompressionMethod   }-  deriving (Show, Generic, FromJSON, ToJSON, ToSchema, NFData)+  deriving (Show, Generic, FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)  data CompressionMethod = XZ | ZSTD-  deriving (Show, Eq, Generic, FromJSON, ToJSON, ToSchema, NFData)+  deriving (Show, Eq, Generic, FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)  instance Read CompressionMethod where   readsPrec _ value =@@ -40,5 +41,8 @@   toUrlPiece XZ = "xz"   toUrlPiece ZSTD = "zst" -instance ToParamSchema CompressionMethod where-  toParamSchema _ = toParamSchema (Proxy :: Proxy Text)+instance Swagger.ToParamSchema CompressionMethod where+  toParamSchema _ = Swagger.toParamSchema (Proxy :: Proxy Text)++instance OpenApi.ToParamSchema CompressionMethod where+  toParamSchema _ = OpenApi.toParamSchema (Proxy :: Proxy Text)
src/Cachix/Types/ByteStringStreaming.hs view
@@ -1,15 +1,22 @@ module Cachix.Types.ByteStringStreaming where  import qualified Data.ByteString.Lazy as LBS-import Data.Swagger (NamedSchema (..), ToSchema (..), binarySchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Protolude  newtype ByteStringStreaming = ByteStringStreaming ByteString -instance ToSchema ByteStringStreaming where-  declareNamedSchema _ = pure $ NamedSchema (Just "ByteString") binarySchema+instance Swagger.ToSchema ByteStringStreaming where+  declareNamedSchema _ = pure $ Swagger.NamedSchema (Just "ByteString") Swagger.binarySchema  newtype LazyByteStringStreaming = LazyByteStringStreaming LBS.ByteString -instance ToSchema LazyByteStringStreaming where-  declareNamedSchema _ = pure $ NamedSchema (Just "lazyByteString") binarySchema+instance Swagger.ToSchema LazyByteStringStreaming where+  declareNamedSchema _ = pure $ Swagger.NamedSchema (Just "lazyByteString") Swagger.binarySchema++instance OpenApi.ToSchema ByteStringStreaming where+  declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "ByteString") OpenApi.binarySchema++instance OpenApi.ToSchema LazyByteStringStreaming where+  declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "lazyByteString") OpenApi.binarySchema
src/Cachix/Types/Deploy.hs view
@@ -3,7 +3,8 @@ module Cachix.Types.Deploy where  import Data.HashMap.Strict-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Deriving.Aeson import Protolude @@ -11,5 +12,5 @@   { agents :: HashMap Text Text,     rollbackScript :: Maybe (HashMap Text Text)   }-  deriving (Show, Eq, Generic, FromJSON, ToSchema)+  deriving (Show, Eq, Generic, FromJSON, Swagger.ToSchema, OpenApi.ToSchema)   deriving (ToJSON) via CustomJSON '[OmitNothingFields] Deploy
src/Cachix/Types/DeployResponse/V1.hs view
@@ -5,11 +5,17 @@     ToJSON,   ) import Data.HashMap.Strict-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Protolude  newtype DeployResponse = DeployResponse   { agents :: HashMap Text Text   }   deriving stock (Show, Generic)-  deriving anyclass (FromJSON, ToJSON, ToSchema, NFData)+  deriving anyclass (FromJSON, ToJSON, Swagger.ToSchema, NFData)++instance OpenApi.ToSchema DeployResponse where+  declareNamedSchema proxy = do+    OpenApi.NamedSchema _ responseSchema <- OpenApi.genericDeclareNamedSchema OpenApi.defaultSchemaOptions proxy+    pure $ OpenApi.NamedSchema (Just "DeployResponseV1") responseSchema
src/Cachix/Types/DeployResponse/V2.hs view
@@ -5,7 +5,8 @@     ToJSON,   ) import Data.HashMap.Strict-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Data.UUID (UUID) import Protolude @@ -14,10 +15,15 @@     url :: Text   }   deriving stock (Show, Generic)-  deriving anyclass (FromJSON, ToJSON, ToSchema, NFData)+  deriving anyclass (FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)  newtype DeployResponse = DeployResponse   { agents :: HashMap Text Details   }   deriving stock (Show, Generic)-  deriving anyclass (FromJSON, ToJSON, ToSchema, NFData)+  deriving anyclass (FromJSON, ToJSON, Swagger.ToSchema, NFData)++instance OpenApi.ToSchema DeployResponse where+  declareNamedSchema proxy = do+    OpenApi.NamedSchema _ responseSchema <- OpenApi.genericDeclareNamedSchema OpenApi.defaultSchemaOptions proxy+    pure $ OpenApi.NamedSchema (Just "DeployResponseV2") responseSchema
src/Cachix/Types/Deployment.hs view
@@ -4,7 +4,8 @@   ( FromJSON,     ToJSON,   )-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Data.Time.Clock (UTCTime) import Data.UUID (UUID) import Protolude@@ -16,7 +17,7 @@   | Failed   | Succeeded   deriving stock (Eq, Generic, Show)-  deriving anyclass (FromJSON, ToJSON, ToSchema, NFData)+  deriving anyclass (FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)  data Deployment = Deployment   { id :: UUID,@@ -29,4 +30,4 @@     status :: Status   }   deriving stock (Show, Generic)-  deriving anyclass (FromJSON, ToJSON, ToSchema, NFData)+  deriving anyclass (FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)
src/Cachix/Types/MultipartUpload.hs view
@@ -2,7 +2,8 @@  import Cachix.Types.NarInfoCreate (NarInfoCreate) import Data.Aeson (FromJSON, ToJSON)-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Data.UUID (UUID) import Protolude @@ -11,18 +12,18 @@     uploadId :: Text   }   deriving stock (Generic, Eq, Show)-  deriving anyclass (ToJSON, FromJSON, ToSchema, NFData)+  deriving anyclass (ToJSON, FromJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)  -- | Any hashes or headers required to create the presigned URL. data SigningData = SigningData   { contentMD5 :: Text   }   deriving stock (Generic, Eq, Show)-  deriving anyclass (ToJSON, FromJSON, ToSchema, NFData)+  deriving anyclass (ToJSON, FromJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)  newtype UploadPartResponse = UploadPartResponse {uploadUrl :: Text}   deriving stock (Generic, Show)-  deriving anyclass (ToJSON, FromJSON, ToSchema, NFData)+  deriving anyclass (ToJSON, FromJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)  data CompletedPart = CompletedPart   { partNumber :: Int,@@ -30,7 +31,7 @@     eTag :: Text   }   deriving stock (Generic, Eq, Show)-  deriving anyclass (ToJSON, FromJSON, ToSchema, NFData)+  deriving anyclass (ToJSON, FromJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)  instance Ord CompletedPart where   p1 `compare` p2 = partNumber p1 `compare` partNumber p2@@ -44,4 +45,4 @@     narInfoCreate :: NarInfoCreate   }   deriving stock (Generic, Show)-  deriving anyclass (ToJSON, FromJSON, ToSchema)+  deriving anyclass (ToJSON, FromJSON, Swagger.ToSchema, OpenApi.ToSchema)
src/Cachix/Types/NarFileName.hs view
@@ -3,7 +3,8 @@   ) where -import Data.Swagger (ToParamSchema (..))+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import qualified Data.Text as T import Protolude import Servant.API@@ -25,5 +26,8 @@ instance ToHttpApiData NarFileName where   toUrlPiece narfilename = contentHash narfilename <> ".nar." <> extension narfilename -instance ToParamSchema NarFileName where-  toParamSchema _ = toParamSchema (Proxy :: Proxy Text)+instance Swagger.ToParamSchema NarFileName where+  toParamSchema _ = Swagger.toParamSchema (Proxy :: Proxy Text)++instance OpenApi.ToParamSchema NarFileName where+  toParamSchema _ = OpenApi.toParamSchema (Proxy :: Proxy Text)
src/Cachix/Types/NarInfo.hs view
@@ -9,11 +9,14 @@ where  import Data.Aeson (FromJSON, ToJSON)-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Nix.NarInfo (NarInfo (..)) import Protolude -deriving instance ToSchema CachixNarInfo+deriving instance Swagger.ToSchema CachixNarInfo++deriving instance OpenApi.ToSchema CachixNarInfo  deriving instance ToJSON CachixNarInfo 
src/Cachix/Types/NarInfoCreate.hs view
@@ -9,7 +9,8 @@   ( FromJSON,     ToJSON,   )-import Data.Swagger+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Protolude  -- TODO: get rid of c prefix@@ -28,7 +29,7 @@     cDeriver :: Text,     cSig :: Maybe Text   }-  deriving (Generic, Show, FromJSON, ToJSON, ToSchema)+  deriving (Generic, Show, FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema)  data NarInfoInvalid   = NarSizeIsZero
src/Cachix/Types/NarInfoHash.hs view
@@ -3,14 +3,15 @@   ) where -import Data.Swagger (ToParamSchema, ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Data.Text (dropEnd, takeEnd) import Protolude import Servant.API  -- | Store path hash newtype NarInfoHash = NarInfoHash {unnarinfohash :: Text}-  deriving (Generic, ToSchema, ToParamSchema)+  deriving (Generic, Swagger.ToSchema, Swagger.ToParamSchema, OpenApi.ToSchema, OpenApi.ToParamSchema)  instance FromHttpApiData NarInfoHash where   parseUrlPiece s =
src/Cachix/Types/NixCacheInfo.hs view
@@ -1,7 +1,8 @@ module Cachix.Types.NixCacheInfo where  import Data.Aeson (FromJSON, ToJSON)-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Protolude  data NixCacheInfo = NixCacheInfo@@ -9,4 +10,4 @@     wantMassQuery :: Integer,     priority :: Integer   }-  deriving (Generic, Show, FromJSON, ToJSON, ToSchema, NFData)+  deriving (Generic, Show, FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData)
src/Cachix/Types/Permission.hs view
@@ -1,8 +1,9 @@ module Cachix.Types.Permission where  import Data.Aeson (FromJSON, ToJSON)-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Protolude  data Permission = Read | Write | Admin-  deriving (Generic, Show, FromJSON, ToJSON, ToSchema, NFData, Eq, Ord)+  deriving (Generic, Show, FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema, NFData, Eq, Ord)
src/Cachix/Types/PinCreate.hs view
@@ -1,15 +1,16 @@ module Cachix.Types.PinCreate where  import Data.Aeson (FromJSON, ToJSON)-import Data.Swagger (ToSchema (..), defaultSchemaOptions, genericDeclareNamedSchemaUnrestricted)+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Protolude  data Keep = Days Int | Revisions Int | Forever-  deriving (Show, Eq, Generic, ToJSON, FromJSON)+  deriving (Show, Eq, Generic, ToJSON, FromJSON, OpenApi.ToSchema)  -- to support Keep with data constructor with arguments-instance ToSchema Keep where-  declareNamedSchema = genericDeclareNamedSchemaUnrestricted defaultSchemaOptions+instance Swagger.ToSchema Keep where+  declareNamedSchema = Swagger.genericDeclareNamedSchemaUnrestricted Swagger.defaultSchemaOptions  data PinCreate = PinCreate   { name :: Text,@@ -17,4 +18,4 @@     artifacts :: [Text],     keep :: Maybe Keep   }-  deriving (Show, Eq, Generic, ToJSON, FromJSON, ToSchema)+  deriving (Show, Eq, Generic, ToJSON, FromJSON, Swagger.ToSchema, OpenApi.ToSchema)
src/Cachix/Types/SigningKeyCreate.hs view
@@ -7,11 +7,12 @@   ( FromJSON,     ToJSON,   )-import Data.Swagger+import qualified Data.OpenApi as OpenApi+import qualified Data.Swagger as Swagger import Protolude  -- | Conveys that a signing secret key was created, by sharing the public key. newtype SigningKeyCreate = SigningKeyCreate   { publicKey :: Text   }-  deriving (Show, Generic, FromJSON, ToJSON, ToSchema)+  deriving (Show, Generic, FromJSON, ToJSON, Swagger.ToSchema, OpenApi.ToSchema)
+ test/WebSocketClientSpec.hs view
@@ -0,0 +1,43 @@+module WebSocketClientSpec where++import Cachix.API.WebSocket.Client+import qualified Data.IORef as IORef+import qualified Network.WebSockets as WS+import qualified Network.WebSockets.Stream as Stream+import Protolude+import Test.Hspec++spec :: Spec+spec = describe "WebSocket handshake errors" $ do+  for_ [(400, "Bad Request"), (401, "Unauthorized")] $ \(status, message) ->+    it ("exposes HTTP " <> show status <> " rejections with their response headers") $ do+      let response = WS.ResponseHead status message [("WWW-Authenticate", "Bearer")]+          rawResponse = "HTTP/1.1 " <> encodeUtf8 (show status :: Text) <> " " <> message <> "\r\nWWW-Authenticate: Bearer\r\n\r\n"+      handshake rawResponse `shouldThrow` rejectsWith response++  it "preserves malformed HTTP 101 upgrade errors" $+    handshake "HTTP/1.1 101 Switching Protocols\r\nSec-WebSocket-Accept: invalid\r\n\r\n"+      `shouldThrow` \case+        WS.MalformedResponse actual _ -> WS.responseCode actual == 101+        _ -> False++  it "preserves failures without an HTTP response" $+    handshake ""+      `shouldThrow` \case+        WS.ParseException _ -> True+        _ -> False++rejectsWith :: WS.ResponseHead -> HandshakeRejected -> Bool+rejectsWith expected (HandshakeRejected actual) =+  WS.responseCode actual == WS.responseCode expected+    && WS.responseMessage actual == WS.responseMessage expected+    && WS.responseHeaders actual == WS.responseHeaders expected++-- Exercise the real client handshake parser without opening a socket.+handshake :: ByteString -> IO ()+handshake response = do+  input <- IORef.newIORef (Just response)+  bracket+    (Stream.makeStream (IORef.atomicModifyIORef' input (\chunk -> (Nothing, chunk))) (const $ pure ()))+    Stream.close+    (\stream -> withHandshakeErrors $ WS.runClientWithStream stream "localhost" "/ws" WS.defaultConnectionOptions [] (const $ pure ()))