Files
teleport/integration/appaccess/appaccess_test.go
T
2022-10-04 11:14:12 -04:00

680 lines
23 KiB
Go

// Copyright 2022 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package appaccess
import (
"bufio"
"context"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/gravitational/oxy/forward"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/integration/helpers"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/teleport/lib/web/app"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)
// TestAppAccess runs the full application access integration test suite.
//
// It allows to make the entire cluster set up once, instead of per test,
// which speeds things up significantly.
func TestAppAccess(t *testing.T) {
pack := Setup(t)
t.Run("Forward", bind(pack, testForward))
t.Run("Websockets", bind(pack, testWebsockets))
t.Run("ClientCert", bind(pack, testClientCert))
t.Run("Flush", bind(pack, testFlush))
t.Run("ForwardModes", bind(pack, testForwardModes))
t.Run("RewriteHeadersRoot", bind(pack, testRewriteHeadersRoot))
t.Run("RewriteHeadersLeaf", bind(pack, testRewriteHeadersLeaf))
t.Run("Logout", bind(pack, testLogout))
t.Run("JWT", bind(pack, testJWT))
t.Run("NoHeaderOverrides", bind(pack, testNoHeaderOverrides))
t.Run("AuditEvents", bind(pack, testAuditEvents))
t.Run("TestAppInvalidateAppSessionsOnLogout", bind(pack, testInvalidateAppSessionsOnLogout))
// This test should go last because it stops/starts app servers.
t.Run("TestAppServersHA", bind(pack, testServersHA))
}
// testForward tests that requests get forwarded to the target application
// within a single cluster and trusted cluster.
func testForward(p *Pack, t *testing.T) {
tests := []struct {
desc string
inCookie string
outStatusCode int
outMessage string
}{
{
desc: "root cluster, valid application session cookie, success",
inCookie: p.CreateAppSession(t, p.rootAppPublicAddr, p.rootAppClusterName),
outStatusCode: http.StatusOK,
outMessage: p.rootMessage,
},
{
desc: "leaf cluster, valid application session cookie, success",
inCookie: p.CreateAppSession(t, p.leafAppPublicAddr, p.leafAppClusterName),
outStatusCode: http.StatusOK,
outMessage: p.leafMessage,
},
{
desc: "invalid application session cookie, redirect to login",
inCookie: "D25C463CD27861559CC6A0A6AE54818079809AA8731CB18037B4B37A80C4FC6C",
outStatusCode: http.StatusFound,
outMessage: "",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
tt := tt
status, body, err := p.MakeRequest(tt.inCookie, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, tt.outStatusCode, status)
require.Contains(t, body, tt.outMessage)
})
}
}
// TestWebsockets makes sure that websocket requests get forwarded.
func testWebsockets(p *Pack, t *testing.T) {
tests := []struct {
desc string
inCookie string
outMessage string
err error
}{
{
desc: "root cluster, valid application session cookie, successful websocket (ws://) request",
inCookie: p.CreateAppSession(t, p.rootWSPublicAddr, p.rootAppClusterName),
outMessage: p.rootWSMessage,
},
{
desc: "root cluster, valid application session cookie, successful secure websocket (wss://) request",
inCookie: p.CreateAppSession(t, p.rootWSSPublicAddr, p.rootAppClusterName),
outMessage: p.rootWSSMessage,
},
{
desc: "leaf cluster, valid application session cookie, successful websocket (ws://) request",
inCookie: p.CreateAppSession(t, p.leafWSPublicAddr, p.leafAppClusterName),
outMessage: p.leafWSMessage,
},
{
desc: "leaf cluster, valid application session cookie, successful secure websocket (wss://) request",
inCookie: p.CreateAppSession(t, p.leafWSSPublicAddr, p.leafAppClusterName),
outMessage: p.leafWSSMessage,
},
{
desc: "invalid application session cookie, websocket request fails to dial",
inCookie: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
err: errors.New(""),
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
tt := tt
body, err := p.makeWebsocketRequest(tt.inCookie, "/")
if tt.err != nil {
require.IsType(t, tt.err, trace.Unwrap(err))
} else {
require.NoError(t, err)
require.Equal(t, tt.outMessage, body)
}
})
}
}
// testForwardModes ensures that requests are forwarded to applications
// even when the cluster is in proxy recording mode.
func testForwardModes(p *Pack, t *testing.T) {
// Create cluster, user, sessions, and credentials package.
ctx := context.Background()
// Update root and leaf clusters to record sessions at the proxy.
recConfig, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
Mode: types.RecordAtProxy,
})
require.NoError(t, err)
err = p.rootCluster.Process.GetAuthServer().SetSessionRecordingConfig(ctx, recConfig)
require.NoError(t, err)
err = p.leafCluster.Process.GetAuthServer().SetSessionRecordingConfig(ctx, recConfig)
require.NoError(t, err)
// Requests to root and leaf cluster are successful.
tests := []struct {
desc string
inCookie string
outStatusCode int
outMessage string
}{
{
desc: "root cluster, valid application session cookie, success",
inCookie: p.CreateAppSession(t, p.rootAppPublicAddr, p.rootAppClusterName),
outStatusCode: http.StatusOK,
outMessage: p.rootMessage,
},
{
desc: "leaf cluster, valid application session cookie, success",
inCookie: p.CreateAppSession(t, p.leafAppPublicAddr, p.leafAppClusterName),
outStatusCode: http.StatusOK,
outMessage: p.leafMessage,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
tt := tt
status, body, err := p.MakeRequest(tt.inCookie, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, tt.outStatusCode, status)
require.Contains(t, body, tt.outMessage)
})
}
}
// testClientCert tests mutual TLS authentication flow with application
// access typically used in CLI by curl and other clients.
func testClientCert(p *Pack, t *testing.T) {
tests := []struct {
desc string
inTLSConfig *tls.Config
outStatusCode int
outMessage string
}{
{
desc: "root cluster, valid TLS config, success",
inTLSConfig: p.makeTLSConfig(t, p.rootAppPublicAddr, p.rootAppClusterName),
outStatusCode: http.StatusOK,
outMessage: p.rootMessage,
},
{
desc: "leaf cluster, valid TLS config, success",
inTLSConfig: p.makeTLSConfig(t, p.leafAppPublicAddr, p.leafAppClusterName),
outStatusCode: http.StatusOK,
outMessage: p.leafMessage,
},
{
desc: "root cluster, invalid session ID",
inTLSConfig: p.makeTLSConfigNoSession(t, p.rootAppPublicAddr, p.rootAppClusterName),
outStatusCode: http.StatusFound,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
tt := tt
status, body, err := p.makeRequestWithClientCert(tt.inTLSConfig, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, tt.outStatusCode, status)
require.Contains(t, body, tt.outMessage)
})
}
}
// appAccessFlush makes sure that application access periodically flushes
// buffered data to the response.
func testFlush(p *Pack, t *testing.T) {
req, err := http.NewRequest("GET", p.assembleRootProxyURL("/"), nil)
require.NoError(t, err)
cookie := p.CreateAppSession(t, p.flushAppPublicAddr, p.flushAppClusterName)
req.AddCookie(&http.Cookie{
Name: app.CookieName,
Value: cookie,
})
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// The "flush server" will send 2 messages, "hello" and "world", with a
// 500ms delay between them. They should arrive as 2 different frames
// due to the periodic flushing.
frames := []string{"hello", "world"}
for _, frame := range frames {
buffer := make([]byte, 1024)
n, err := resp.Body.Read(buffer)
if err != nil {
require.ErrorIs(t, err, io.EOF)
}
require.Equal(t, frame, strings.TrimSpace(string(buffer[:n])))
}
}
// testRewriteHeadersRoot validates that http headers from application
// rewrite configuration are correctly passed to proxied applications in root.
func testRewriteHeadersRoot(p *Pack, t *testing.T) {
// Create an application session for dumper app in root cluster.
appCookie := p.CreateAppSession(t, "dumper-root.example.com", "example.com")
// Get headers response and make sure headers were passed.
status, resp, err := p.MakeRequest(appCookie, http.MethodGet, "/", service.Header{
Name: "X-Existing", Value: "existing",
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
// Dumper app just dumps HTTP request so we should be able to read it back.
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(resp)))
require.NoError(t, err)
require.Equal(t, req.Host, "example.com")
require.Equal(t, req.Header.Get("X-Teleport-Cluster"), "root")
require.Equal(t, req.Header.Get("X-External-Env"), "production")
require.Equal(t, req.Header.Get("X-Existing"), "rewritten-existing-header")
require.NotEqual(t, req.Header.Get(teleport.AppJWTHeader), "rewritten-app-jwt-header")
require.NotEqual(t, req.Header.Get(teleport.AppCFHeader), "rewritten-app-cf-header")
require.NotEqual(t, req.Header.Get(forward.XForwardedFor), "rewritten-x-forwarded-for-header")
require.NotEqual(t, req.Header.Get(forward.XForwardedHost), "rewritten-x-forwarded-host-header")
require.NotEqual(t, req.Header.Get(forward.XForwardedProto), "rewritten-x-forwarded-proto-header")
require.NotEqual(t, req.Header.Get(forward.XForwardedServer), "rewritten-x-forwarded-server-header")
// Verify JWT tokens.
for _, header := range []string{teleport.AppJWTHeader, teleport.AppCFHeader, "X-JWT"} {
verifyJWT(t, p, req.Header.Get(header), p.dumperAppURI)
}
}
// testRewriteHeadersLeaf validates that http headers from application
// rewrite configuration are correctly passed to proxied applications in leaf.
func testRewriteHeadersLeaf(p *Pack, t *testing.T) {
// Create an application session for dumper app in leaf cluster.
appCookie := p.CreateAppSession(t, "dumper-leaf.example.com", "leaf.example.com")
// Get headers response and make sure headers were passed.
status, resp, err := p.MakeRequest(appCookie, http.MethodGet, "/", service.Header{
Name: "X-Existing", Value: "existing",
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
require.Contains(t, resp, "X-Teleport-Cluster: leaf")
require.Contains(t, resp, "X-Teleport-Login: root")
require.Contains(t, resp, "X-Teleport-Login: ubuntu")
require.Contains(t, resp, "X-External-Env: production")
require.Contains(t, resp, "Host: example.com")
require.Contains(t, resp, "X-Existing: rewritten-existing-header")
require.NotContains(t, resp, "X-Existing: existing")
require.NotContains(t, resp, "rewritten-app-jwt-header")
require.NotContains(t, resp, "rewritten-app-cf-header")
require.NotContains(t, resp, "rewritten-x-forwarded-for-header")
require.NotContains(t, resp, "rewritten-x-forwarded-host-header")
require.NotContains(t, resp, "rewritten-x-forwarded-proto-header")
require.NotContains(t, resp, "rewritten-x-forwarded-server-header")
}
// testLogout verifies the session is removed from the backend when the user logs out.
func testLogout(p *Pack, t *testing.T) {
// Create an application session.
appCookie := p.CreateAppSession(t, p.rootAppPublicAddr, p.rootAppClusterName)
// Log user out of session.
status, _, err := p.MakeRequest(appCookie, http.MethodGet, "/teleport-logout")
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
// Wait until requests using the session cookie have failed.
status, err = p.waitForLogout(appCookie)
require.NoError(t, err)
require.Equal(t, http.StatusFound, status)
}
// testJWT ensures a JWT token is attached to requests and the JWT token can
// be validated.
func testJWT(p *Pack, t *testing.T) {
// Create an application session.
appCookie := p.CreateAppSession(t, p.jwtAppPublicAddr, p.jwtAppClusterName)
// Get JWT.
status, token, err := p.MakeRequest(appCookie, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
// Verify JWT token.
verifyJWT(t, p, token, p.jwtAppURI)
// Connect to websocket application that dumps the upgrade request.
wsCookie := p.CreateAppSession(t, p.wsHeaderAppPublicAddr, p.wsHeaderAppClusterName)
body, err := p.makeWebsocketRequest(wsCookie, "/")
require.NoError(t, err)
// Parse the upgrade request the websocket application received.
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(body)))
require.NoError(t, err)
// Extract JWT token from header and verify it.
wsToken := req.Header.Get(teleport.AppJWTHeader)
require.NotEmpty(t, wsToken, "websocket upgrade request doesn't contain JWT header")
verifyJWT(t, p, wsToken, p.wsHeaderAppURI)
}
// testNoHeaderOverrides ensures that AAP-specific headers cannot be overridden
// by values passed in by the user.
func testNoHeaderOverrides(p *Pack, t *testing.T) {
// Create an application session.
appCookie := p.CreateAppSession(t, p.headerAppPublicAddr, p.headerAppClusterName)
// Get HTTP headers forwarded to the application.
status, origHeaderResp, err := p.MakeRequest(appCookie, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
origHeaders := strings.Split(origHeaderResp, "\n")
require.Equal(t, len(origHeaders), len(forwardedHeaderNames)+1)
// Construct HTTP request with custom headers.
req, err := http.NewRequest(http.MethodGet, p.assembleRootProxyURL("/"), nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: app.CookieName,
Value: appCookie,
})
for _, headerName := range forwardedHeaderNames {
req.Header.Set(headerName, uuid.New().String())
}
// Issue the request.
status, newHeaderResp, err := p.sendRequest(req, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
newHeaders := strings.Split(newHeaderResp, "\n")
require.Equal(t, len(newHeaders), len(forwardedHeaderNames)+1)
// Headers sent to the application should not be affected.
for i := range forwardedHeaderNames {
require.Equal(t, origHeaders[i], newHeaders[i])
}
}
func testAuditEvents(p *Pack, t *testing.T) {
inCookie := p.CreateAppSession(t, p.rootAppPublicAddr, p.rootAppClusterName)
status, body, err := p.MakeRequest(inCookie, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
require.Contains(t, body, p.rootMessage)
// session start event
p.ensureAuditEvent(t, events.AppSessionStartEvent, func(event apievents.AuditEvent) {
expectedEvent := &apievents.AppSessionStart{
Metadata: apievents.Metadata{
Type: events.AppSessionStartEvent,
Code: events.AppSessionStartCode,
ClusterName: p.rootAppClusterName,
},
AppMetadata: apievents.AppMetadata{
AppURI: p.rootAppURI,
AppPublicAddr: p.rootAppPublicAddr,
AppName: p.rootAppName,
},
PublicAddr: p.rootAppPublicAddr,
}
require.Empty(t, cmp.Diff(
expectedEvent,
event,
cmpopts.IgnoreTypes(apievents.ServerMetadata{}, apievents.SessionMetadata{}, apievents.UserMetadata{}, apievents.ConnectionMetadata{}),
cmpopts.IgnoreFields(apievents.Metadata{}, "ID", "Time"),
))
})
// session chunk event
p.ensureAuditEvent(t, events.AppSessionChunkEvent, func(event apievents.AuditEvent) {
expectedEvent := &apievents.AppSessionChunk{
Metadata: apievents.Metadata{
Type: events.AppSessionChunkEvent,
Code: events.AppSessionChunkCode,
ClusterName: p.rootAppClusterName,
},
AppMetadata: apievents.AppMetadata{
AppURI: p.rootAppURI,
AppPublicAddr: p.rootAppPublicAddr,
AppName: p.rootAppName,
},
}
require.Empty(t, cmp.Diff(
expectedEvent,
event,
cmpopts.IgnoreTypes(apievents.ServerMetadata{}, apievents.SessionMetadata{}, apievents.UserMetadata{}, apievents.ConnectionMetadata{}),
cmpopts.IgnoreFields(apievents.Metadata{}, "ID", "Time"),
cmpopts.IgnoreFields(apievents.AppSessionChunk{}, "SessionChunkID"),
))
})
}
func testInvalidateAppSessionsOnLogout(p *Pack, t *testing.T) {
t.Cleanup(func() {
// This test will invalidate the web session so init it again after the
// test, otherwise tests that run after this one will be getting 403's.
p.initWebSession(t)
})
// Create an application session.
appCookie := p.CreateAppSession(t, p.rootAppPublicAddr, p.rootAppClusterName)
// Issue a request to the application to guarantee everything is working correctly.
status, _, err := p.MakeRequest(appCookie, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
// Generates TLS config for making app requests.
reqTLS := p.makeTLSConfig(t, p.rootAppPublicAddr, p.rootAppClusterName)
require.NotNil(t, reqTLS)
// Issue a request to the application to guarantee everything is working correctly.
status, _, err = p.makeRequestWithClientCert(reqTLS, http.MethodGet, "/")
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
// Logout from Teleport.
status, _, err = p.makeWebapiRequest(http.MethodDelete, "sessions", []byte{})
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
// As deleting WebSessions might not happen immediately, run the next request
// in an `Eventually` block.
require.Eventually(t, func() bool {
// Issue another request to the application. Now, it should receive a
// redirect because the application sessions are gone.
status, _, err = p.MakeRequest(appCookie, http.MethodGet, "/")
require.NoError(t, err)
return status == http.StatusFound
}, time.Second, 250*time.Millisecond)
// Check the same for the client certificate.
require.Eventually(t, func() bool {
// Issue another request to the application. Now, it should receive a
// redirect because the application sessions are gone.
status, _, err = p.makeRequestWithClientCert(reqTLS, http.MethodGet, "/")
require.NoError(t, err)
return status == http.StatusFound
}, time.Second, 250*time.Millisecond)
}
// TestTCP tests proxying of plain TCP applications through app access.
func TestTCP(t *testing.T) {
pack := Setup(t)
tests := []struct {
description string
address string
outMessage string
}{
{
description: "TCP app in root cluster",
address: pack.startLocalProxy(t, pack.rootTCPPublicAddr, pack.rootAppClusterName),
outMessage: pack.rootTCPMessage,
},
{
description: "TCP app in leaf cluster",
address: pack.startLocalProxy(t, pack.leafTCPPublicAddr, pack.leafAppClusterName),
outMessage: pack.leafTCPMessage,
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
conn, err := net.Dial("tcp", test.address)
require.NoError(t, err)
buf := make([]byte, 1024)
n, err := conn.Read(buf)
require.NoError(t, err)
resp := strings.TrimSpace(string(buf[:n]))
require.Equal(t, test.outMessage, resp)
})
}
}
func testServersHA(p *Pack, t *testing.T) {
type packInfo struct {
clusterName string
publicHTTPAddr string
publicWSAddr string
appServers []*service.TeleportProcess
}
testCases := map[string]struct {
packInfo func(pack *Pack) packInfo
startAppServers func(pack *Pack, count int) []*service.TeleportProcess
waitForTunnelConn func(t *testing.T, pack *Pack, count int)
}{
"RootServer": {
packInfo: func(pack *Pack) packInfo {
return packInfo{
clusterName: pack.rootAppClusterName,
publicHTTPAddr: pack.rootAppPublicAddr,
publicWSAddr: pack.rootWSPublicAddr,
appServers: pack.rootAppServers,
}
},
startAppServers: func(pack *Pack, count int) []*service.TeleportProcess {
return pack.startRootAppServers(t, count, []service.App{})
},
waitForTunnelConn: func(t *testing.T, pack *Pack, count int) {
helpers.WaitForActiveTunnelConnections(t, pack.rootCluster.Tunnel, pack.rootCluster.Secrets.SiteName, count)
},
},
"LeafServer": {
packInfo: func(pack *Pack) packInfo {
return packInfo{
clusterName: pack.leafAppClusterName,
publicHTTPAddr: pack.leafAppPublicAddr,
publicWSAddr: pack.leafWSPublicAddr,
appServers: pack.leafAppServers,
}
},
startAppServers: func(pack *Pack, count int) []*service.TeleportProcess {
return pack.startLeafAppServers(t, count, []service.App{})
},
waitForTunnelConn: func(t *testing.T, pack *Pack, count int) {
helpers.WaitForActiveTunnelConnections(t, pack.leafCluster.Tunnel, pack.leafCluster.Secrets.SiteName, count)
},
},
}
// asserts that the response has error.
responseWithError := func(t *testing.T, status int, err error) {
if status > 0 {
require.NoError(t, err)
require.Equal(t, http.StatusInternalServerError, status)
return
}
require.Error(t, err)
}
// asserts that the response has no errors.
responseWithoutError := func(t *testing.T, status int, err error) {
if status > 0 {
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
return
}
require.NoError(t, err)
}
makeRequests := func(t *testing.T, pack *Pack, httpCookie, wsCookie string, responseAssertion func(*testing.T, int, error)) {
status, _, err := pack.MakeRequest(httpCookie, http.MethodGet, "/")
responseAssertion(t, status, err)
_, err = pack.makeWebsocketRequest(wsCookie, "/")
responseAssertion(t, 0, err)
}
for name, test := range testCases {
name, test := name, test
t.Run(name, func(t *testing.T) {
info := test.packInfo(p)
httpCookie := p.CreateAppSession(t, info.publicHTTPAddr, info.clusterName)
wsCookie := p.CreateAppSession(t, info.publicWSAddr, info.clusterName)
makeRequests(t, p, httpCookie, wsCookie, responseWithoutError)
// Stop all root app servers.
for i, appServer := range info.appServers {
require.NoError(t, appServer.Close())
require.NoError(t, appServer.Wait())
if i == len(info.appServers)-1 {
// fails only when the last one is closed.
makeRequests(t, p, httpCookie, wsCookie, responseWithError)
} else {
// otherwise the request should be handled by another
// server.
makeRequests(t, p, httpCookie, wsCookie, responseWithoutError)
}
}
servers := test.startAppServers(p, 1)
test.waitForTunnelConn(t, p, 1)
makeRequests(t, p, httpCookie, wsCookie, responseWithoutError)
// Start an additional app server and stop all current running
// ones.
test.startAppServers(p, 1)
test.waitForTunnelConn(t, p, 2)
for _, appServer := range servers {
require.NoError(t, appServer.Close())
require.NoError(t, appServer.Wait())
// Everytime an app server stops we issue a request to
// guarantee that the requests are going to be resolved by
// the remaining app servers.
makeRequests(t, p, httpCookie, wsCookie, responseWithoutError)
}
})
}
}