mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
f6556fce9f
The test flake can be verified by setting `ReportInterval` to a really low value, like `100 * time.Millisecond`. We now set it to a really high value to avoid triggering flush without manually calling the function in test. This can easily happen because the default value is 30s and we run tests in parallel. The assertion typically happens such that: [use workspace] -> [fetch previous last used] -> [flush] -> [fetch new last used] When this edge case is triggered: [use workspace] -> [report interval flush] -> [fetch previous last used] -> [flush] -> [fetch new last used] In this case, both the previous and new last used will be the same, breaking the test assertion. Fixes coder/internal#960 Fixes coder/internal#975
613 lines
19 KiB
Go
613 lines
19 KiB
Go
package apptest
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/agent"
|
|
agentproto "github.com/coder/coder/v2/agent/proto"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/workspaceapps"
|
|
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
|
"github.com/coder/coder/v2/cryptorand"
|
|
"github.com/coder/coder/v2/provisioner/echo"
|
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
const (
|
|
proxyTestAgentName = "agent-name"
|
|
proxyTestAppNameFake = "test-app-fake"
|
|
proxyTestAppNameOwner = "test-app-owner"
|
|
proxyTestAppNameAuthenticated = "test-app-authenticated"
|
|
proxyTestAppNamePublic = "test-app-public"
|
|
// nolint:gosec // Not a secret
|
|
proxyTestAppNameAuthenticatedCORSPassthru = "test-app-authenticated-cors-passthru"
|
|
proxyTestAppNamePublicCORSPassthru = "test-app-public-cors-passthru"
|
|
proxyTestAppNameAuthenticatedCORSDefault = "test-app-authenticated-cors-default"
|
|
proxyTestAppNamePublicCORSDefault = "test-app-public-cors-default"
|
|
proxyTestAppQuery = "query=true"
|
|
proxyTestAppBody = "hello world from apps test"
|
|
|
|
proxyTestSubdomainRaw = "*.test.coder.com"
|
|
proxyTestSubdomain = "test.coder.com"
|
|
)
|
|
|
|
// DeploymentOptions are the options for creating a *Deployment with a
|
|
// DeploymentFactory.
|
|
type DeploymentOptions struct {
|
|
PrimaryAppHost string
|
|
AppHost string
|
|
DisablePathApps bool
|
|
DisableSubdomainApps bool
|
|
DangerousAllowPathAppSharing bool
|
|
DangerousAllowPathAppSiteOwnerAccess bool
|
|
ServeHTTPS bool
|
|
|
|
StatsCollectorOptions workspaceapps.StatsCollectorOptions
|
|
|
|
// The following fields are only used by setupProxyTestWithFactory.
|
|
noWorkspace bool
|
|
port uint16
|
|
headers http.Header
|
|
handler http.Handler
|
|
}
|
|
|
|
// Deployment is a license-agnostic deployment with all the fields that apps
|
|
// tests need.
|
|
type Deployment struct {
|
|
Options *DeploymentOptions
|
|
|
|
// SDKClient should be logged in as the admin user.
|
|
SDKClient *codersdk.Client
|
|
FirstUser codersdk.CreateFirstUserResponse
|
|
PathAppBaseURL *url.URL
|
|
FlushStats func()
|
|
}
|
|
|
|
// DeploymentFactory generates a deployment with an API client, a path base URL,
|
|
// and a subdomain app host URL.
|
|
type DeploymentFactory func(t *testing.T, opts *DeploymentOptions) *Deployment
|
|
|
|
// App is similar to httpapi.ApplicationURL but with a Query field.
|
|
type App struct {
|
|
Username string
|
|
WorkspaceName string
|
|
// AgentName is optional, except for when proxying to a port. AgentName is
|
|
// always ignored when making a path app URL.
|
|
//
|
|
// Set WorkspaceName to `workspace.agent` if you want to generate a path app
|
|
// URL with an agent name.
|
|
AgentName string
|
|
AppSlugOrPort string
|
|
|
|
// Prefix should have ---.
|
|
Prefix string
|
|
Query string
|
|
|
|
// Control the behavior of CORS handling.
|
|
CORSBehavior codersdk.CORSBehavior
|
|
}
|
|
|
|
// Details are the full test details returned from setupProxyTestWithFactory.
|
|
type Details struct {
|
|
*Deployment
|
|
|
|
Me codersdk.User
|
|
|
|
// The following fields are not set if setupProxyTest was called with
|
|
// `withWorkspace` set to `false`.
|
|
|
|
Workspace *codersdk.Workspace
|
|
Agent *codersdk.WorkspaceAgent
|
|
AppPort uint16
|
|
|
|
Apps struct {
|
|
Fake App
|
|
Owner App
|
|
Authenticated App
|
|
Public App
|
|
Port App
|
|
PortHTTPS App
|
|
PublicCORSPassthru App
|
|
AuthenticatedCORSPassthru App
|
|
PublicCORSDefault App
|
|
AuthenticatedCORSDefault App
|
|
}
|
|
}
|
|
|
|
// AppClient returns a *codersdk.Client that will route all requests to the
|
|
// app server. API requests will fail with this client. Any redirect responses
|
|
// are not followed by default.
|
|
//
|
|
// The client is authenticated as the first user by default.
|
|
func (d *Details) AppClient(t *testing.T) *codersdk.Client {
|
|
client := codersdk.New(d.PathAppBaseURL, codersdk.WithSessionToken(d.SDKClient.SessionToken()))
|
|
forceURLTransport(t, client)
|
|
client.HTTPClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
// PathAppURL returns the URL for the given path app.
|
|
func (d *Details) PathAppURL(app App) *url.URL {
|
|
appPath := fmt.Sprintf("/@%s/%s/apps/%s", app.Username, app.WorkspaceName, app.AppSlugOrPort)
|
|
|
|
u := *d.PathAppBaseURL
|
|
u.Path = path.Join(u.Path, appPath)
|
|
u.Path += "/"
|
|
u.RawQuery = app.Query
|
|
return &u
|
|
}
|
|
|
|
// SubdomainAppURL returns the URL for the given subdomain app.
|
|
func (d *Details) SubdomainAppURL(app App) *url.URL {
|
|
// Agent name is optional when app slug is present
|
|
agentName := app.AgentName
|
|
if !appurl.PortRegex.MatchString(app.AppSlugOrPort) {
|
|
agentName = ""
|
|
}
|
|
|
|
appHost := appurl.ApplicationURL{
|
|
Prefix: app.Prefix,
|
|
AppSlugOrPort: app.AppSlugOrPort,
|
|
AgentName: agentName,
|
|
WorkspaceName: app.WorkspaceName,
|
|
Username: app.Username,
|
|
}
|
|
u := *d.PathAppBaseURL
|
|
u.Host = strings.Replace(d.Options.AppHost, "*", appHost.String(), 1)
|
|
u.Path = "/"
|
|
u.RawQuery = app.Query
|
|
return &u
|
|
}
|
|
|
|
// setupProxyTestWithFactory does the following:
|
|
// 1. Create a deployment with the factory.
|
|
// 2. Start a test app server.
|
|
// 3. Create a template version, template and workspace with many apps.
|
|
// 4. Start a workspace agent.
|
|
// 5. Returns details about the deployment and its apps.
|
|
func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *DeploymentOptions) *Details {
|
|
if opts == nil {
|
|
opts = &DeploymentOptions{}
|
|
}
|
|
if opts.AppHost == "" {
|
|
opts.AppHost = proxyTestSubdomainRaw
|
|
}
|
|
if opts.DisableSubdomainApps {
|
|
opts.AppHost = ""
|
|
}
|
|
if opts.StatsCollectorOptions.ReportInterval == 0 {
|
|
// Set to a really high value to avoid triggering flush without manually
|
|
// calling the function in test. This can easily happen because the
|
|
// default value is 30s and we run tests in parallel. The assertion
|
|
// typically happens such that:
|
|
//
|
|
// [use workspace] -> [fetch previous last used] -> [flush] -> [fetch new last used]
|
|
//
|
|
// When this edge case is triggered:
|
|
//
|
|
// [use workspace] -> [report interval flush] -> [fetch previous last used] -> [flush] -> [fetch new last used]
|
|
//
|
|
// In this case, both the previous and new last used will be the same,
|
|
// breaking the test assertion.
|
|
opts.StatsCollectorOptions.ReportInterval = 9001 * time.Hour
|
|
}
|
|
|
|
deployment := factory(t, opts)
|
|
|
|
// Configure the HTTP client to not follow redirects and to route all
|
|
// requests regardless of hostname to the coderd test server.
|
|
deployment.SDKClient.HTTPClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
forceURLTransport(t, deployment.SDKClient)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
|
defer cancel()
|
|
|
|
me, err := deployment.SDKClient.User(ctx, codersdk.Me)
|
|
require.NoError(t, err)
|
|
|
|
if opts.noWorkspace {
|
|
return &Details{
|
|
Deployment: deployment,
|
|
Me: me,
|
|
}
|
|
}
|
|
|
|
if opts.port == 0 {
|
|
opts.port = appServer(t, opts.headers, opts.ServeHTTPS, opts.handler)
|
|
}
|
|
workspace, agnt := createWorkspaceWithApps(t, deployment.SDKClient, deployment.FirstUser.OrganizationID, me, opts.port, opts.ServeHTTPS)
|
|
|
|
details := &Details{
|
|
Deployment: deployment,
|
|
Me: me,
|
|
Workspace: &workspace,
|
|
Agent: &agnt,
|
|
AppPort: opts.port,
|
|
}
|
|
|
|
details.Apps.Fake = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: proxyTestAppNameFake,
|
|
}
|
|
details.Apps.Owner = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: "", // Agent name is optional when app slug is present
|
|
AppSlugOrPort: proxyTestAppNameOwner,
|
|
Query: proxyTestAppQuery,
|
|
}
|
|
details.Apps.Authenticated = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: proxyTestAppNameAuthenticated,
|
|
Query: proxyTestAppQuery,
|
|
}
|
|
details.Apps.Public = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: proxyTestAppNamePublic,
|
|
Query: proxyTestAppQuery,
|
|
}
|
|
details.Apps.Port = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: strconv.Itoa(int(opts.port)),
|
|
}
|
|
details.Apps.PortHTTPS = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: strconv.Itoa(int(opts.port)) + "s",
|
|
}
|
|
details.Apps.PublicCORSPassthru = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: proxyTestAppNamePublicCORSPassthru,
|
|
CORSBehavior: codersdk.CORSBehaviorPassthru,
|
|
Query: proxyTestAppQuery,
|
|
}
|
|
details.Apps.AuthenticatedCORSPassthru = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: proxyTestAppNameAuthenticatedCORSPassthru,
|
|
CORSBehavior: codersdk.CORSBehaviorPassthru,
|
|
Query: proxyTestAppQuery,
|
|
}
|
|
details.Apps.PublicCORSDefault = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: proxyTestAppNamePublicCORSDefault,
|
|
Query: proxyTestAppQuery,
|
|
}
|
|
details.Apps.AuthenticatedCORSDefault = App{
|
|
Username: me.Username,
|
|
WorkspaceName: workspace.Name,
|
|
AgentName: agnt.Name,
|
|
AppSlugOrPort: proxyTestAppNameAuthenticatedCORSDefault,
|
|
Query: proxyTestAppQuery,
|
|
}
|
|
|
|
return details
|
|
}
|
|
|
|
//nolint:revive
|
|
func appServer(t *testing.T, headers http.Header, isHTTPS bool, handler http.Handler) uint16 {
|
|
defaultHandler := http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
_, err := r.Cookie(codersdk.SessionTokenCookie)
|
|
assert.ErrorIs(t, err, http.ErrNoCookie)
|
|
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
|
w.Header().Set("X-Got-Host", r.Host)
|
|
for name, values := range headers {
|
|
for _, value := range values {
|
|
w.Header().Add(name, value)
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(proxyTestAppBody))
|
|
},
|
|
)
|
|
|
|
if handler == nil {
|
|
handler = defaultHandler
|
|
}
|
|
|
|
server := httptest.NewUnstartedServer(handler)
|
|
|
|
server.Config.ReadHeaderTimeout = time.Minute
|
|
if isHTTPS {
|
|
server.StartTLS()
|
|
} else {
|
|
server.Start()
|
|
}
|
|
t.Cleanup(func() {
|
|
server.Close()
|
|
})
|
|
|
|
_, portStr, err := net.SplitHostPort(server.Listener.Addr().String())
|
|
require.NoError(t, err)
|
|
port, err := strconv.ParseUint(portStr, 10, 16)
|
|
require.NoError(t, err)
|
|
|
|
return uint16(port)
|
|
}
|
|
|
|
//nolint:revive
|
|
func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.UUID, me codersdk.User, port uint16, serveHTTPS bool, workspaceMutators ...func(*codersdk.CreateWorkspaceRequest)) (codersdk.Workspace, codersdk.WorkspaceAgent) {
|
|
authToken := uuid.NewString()
|
|
|
|
scheme := "http"
|
|
if serveHTTPS {
|
|
scheme = "https"
|
|
}
|
|
|
|
// Workspace name needs to be short to avoid hitting 62 char hostname
|
|
// segment limit.
|
|
workspaceName, err := cryptorand.String(6)
|
|
require.NoError(t, err)
|
|
workspaceName = "ws-" + workspaceName
|
|
workspaceMutators = append([]func(*codersdk.CreateWorkspaceRequest){
|
|
func(req *codersdk.CreateWorkspaceRequest) {
|
|
req.Name = workspaceName
|
|
},
|
|
}, workspaceMutators...)
|
|
|
|
// Intentionally going to choose a port that will never be chosen.
|
|
// Ports <1k will never be selected. 396 is for some old OS over IP.
|
|
// It will never likely be provisioned. Using quick timeout since
|
|
// it's all localhost
|
|
fakeAppURL := "http://127.1.0.1:396"
|
|
conn, err := net.DialTimeout("tcp", fakeAppURL, time.Millisecond*100)
|
|
if err == nil {
|
|
// In the absolute rare case someone hits this. Writing code to find a free port
|
|
// seems like a waste of time to program and run.
|
|
_ = conn.Close()
|
|
t.Errorf("an unused port is required for the fake app. "+
|
|
"The url %q happens to be an active port. If you hit this, then this test"+
|
|
"will need to be modified to run on your system. Or you can stop serving an"+
|
|
"app on that port.", fakeAppURL)
|
|
t.FailNow()
|
|
}
|
|
|
|
appURL := fmt.Sprintf("%s://127.0.0.1:%d?%s", scheme, port, proxyTestAppQuery)
|
|
protoApps := []*proto.App{
|
|
{
|
|
Slug: proxyTestAppNameFake,
|
|
DisplayName: proxyTestAppNameFake,
|
|
SharingLevel: proto.AppSharingLevel_OWNER,
|
|
Url: fakeAppURL,
|
|
Subdomain: true,
|
|
},
|
|
{
|
|
Slug: proxyTestAppNameOwner,
|
|
DisplayName: proxyTestAppNameOwner,
|
|
SharingLevel: proto.AppSharingLevel_OWNER,
|
|
Url: appURL,
|
|
Subdomain: true,
|
|
},
|
|
{
|
|
Slug: proxyTestAppNameAuthenticated,
|
|
DisplayName: proxyTestAppNameAuthenticated,
|
|
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
|
Url: appURL,
|
|
Subdomain: true,
|
|
},
|
|
{
|
|
Slug: proxyTestAppNamePublic,
|
|
DisplayName: proxyTestAppNamePublic,
|
|
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
|
Url: appURL,
|
|
Subdomain: true,
|
|
},
|
|
{
|
|
Slug: proxyTestAppNamePublicCORSPassthru,
|
|
DisplayName: proxyTestAppNamePublicCORSPassthru,
|
|
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
|
Url: appURL,
|
|
Subdomain: true,
|
|
// CorsBehavior: proto.AppCORSBehavior_PASSTHRU,
|
|
},
|
|
{
|
|
Slug: proxyTestAppNameAuthenticatedCORSPassthru,
|
|
DisplayName: proxyTestAppNameAuthenticatedCORSPassthru,
|
|
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
|
Url: appURL,
|
|
Subdomain: true,
|
|
// CorsBehavior: proto.AppCORSBehavior_PASSTHRU,
|
|
},
|
|
{
|
|
Slug: proxyTestAppNamePublicCORSDefault,
|
|
DisplayName: proxyTestAppNamePublicCORSDefault,
|
|
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
|
Url: appURL,
|
|
Subdomain: true,
|
|
},
|
|
{
|
|
Slug: proxyTestAppNameAuthenticatedCORSDefault,
|
|
DisplayName: proxyTestAppNameAuthenticatedCORSDefault,
|
|
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
|
Url: appURL,
|
|
Subdomain: true,
|
|
},
|
|
}
|
|
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "example",
|
|
Type: "aws_instance",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Name: proxyTestAgentName,
|
|
Auth: &proto.Agent_Token{
|
|
Token: authToken,
|
|
},
|
|
Apps: protoApps,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID, workspaceMutators...)
|
|
workspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Verify app subdomains
|
|
for _, app := range workspaceBuild.Resources[0].Agents[0].Apps {
|
|
require.True(t, app.Subdomain)
|
|
|
|
appURL := appurl.ApplicationURL{
|
|
Prefix: "",
|
|
// findProtoApp is needed as the order of apps returned from PG database
|
|
// is not guaranteed.
|
|
AppSlugOrPort: findProtoApp(t, protoApps, app.Slug).Slug,
|
|
WorkspaceName: workspace.Name,
|
|
Username: me.Username,
|
|
}
|
|
require.Equal(t, appURL.String(), app.SubdomainName)
|
|
}
|
|
|
|
agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(authToken))
|
|
|
|
// TODO (@dean): currently, the primary app host is used when generating
|
|
// the port URL we tell the agent to use. We don't have any plans to change
|
|
// that until we let templates pick which proxy they want to use in the
|
|
// terraform.
|
|
//
|
|
// This means that all port URLs generated in code-server etc. will be sent
|
|
// to the primary.
|
|
appHostCtx := testutil.Context(t, testutil.WaitLong)
|
|
primaryAppHost, err := client.AppHost(appHostCtx)
|
|
require.NoError(t, err)
|
|
if primaryAppHost.Host != "" {
|
|
rpcConn, err := agentClient.ConnectRPC(appHostCtx)
|
|
require.NoError(t, err)
|
|
aAPI := agentproto.NewDRPCAgentClient(rpcConn)
|
|
manifest, err := aAPI.GetManifest(appHostCtx, &agentproto.GetManifestRequest{})
|
|
require.NoError(t, err)
|
|
|
|
appHost := appurl.ApplicationURL{
|
|
Prefix: "",
|
|
AppSlugOrPort: "{{port}}",
|
|
AgentName: proxyTestAgentName,
|
|
WorkspaceName: workspace.Name,
|
|
Username: me.Username,
|
|
}
|
|
proxyURL := "http://" + appHost.String() + strings.ReplaceAll(primaryAppHost.Host, "*", "")
|
|
require.Equal(t, manifest.VsCodePortProxyUri, proxyURL)
|
|
err = rpcConn.Close()
|
|
require.NoError(t, err)
|
|
}
|
|
agentCloser := agent.New(agent.Options{
|
|
Client: agentClient,
|
|
Logger: testutil.Logger(t).Named("agent"),
|
|
})
|
|
t.Cleanup(func() {
|
|
_ = agentCloser.Close()
|
|
})
|
|
|
|
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
agents := make([]codersdk.WorkspaceAgent, 0, 1)
|
|
for _, resource := range resources {
|
|
agents = append(agents, resource.Agents...)
|
|
}
|
|
require.Len(t, agents, 1)
|
|
|
|
return workspace, agents[0]
|
|
}
|
|
|
|
func findProtoApp(t *testing.T, protoApps []*proto.App, slug string) *proto.App {
|
|
for _, protoApp := range protoApps {
|
|
if protoApp.Slug == slug {
|
|
return protoApp
|
|
}
|
|
}
|
|
require.FailNowf(t, "proto app not found (slug: %q)", slug)
|
|
return nil
|
|
}
|
|
|
|
func doWithRetries(t require.TestingT, client *codersdk.Client, req *http.Request) (*http.Response, error) {
|
|
var resp *http.Response
|
|
var err error
|
|
require.Eventually(t, func() bool {
|
|
// nolint // only requests which are not passed upstream have a body closed
|
|
resp, err = client.HTTPClient.Do(req)
|
|
if resp != nil && resp.StatusCode == http.StatusBadGateway {
|
|
if resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}, testutil.WaitLong, testutil.IntervalFast)
|
|
return resp, err
|
|
}
|
|
|
|
func requestWithRetries(ctx context.Context, t testing.TB, client *codersdk.Client, method, urlOrPath string, body interface{}, opts ...codersdk.RequestOption) (*http.Response, error) {
|
|
t.Helper()
|
|
var resp *http.Response
|
|
var err error
|
|
require.Eventually(t, func() bool {
|
|
// nolint // only requests which are not passed upstream have a body closed
|
|
resp, err = client.Request(ctx, method, urlOrPath, body, opts...)
|
|
if resp != nil && resp.StatusCode == http.StatusBadGateway {
|
|
if resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}, testutil.WaitLong, testutil.IntervalFast)
|
|
return resp, err
|
|
}
|
|
|
|
// forceURLTransport forces the client to route all requests to the client's
|
|
// configured URLs host regardless of hostname.
|
|
func forceURLTransport(t *testing.T, client *codersdk.Client) {
|
|
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
|
|
require.True(t, ok)
|
|
transport := defaultTransport.Clone()
|
|
transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
|
|
return (&net.Dialer{}).DialContext(ctx, network, client.URL.Host)
|
|
}
|
|
client.HTTPClient.Transport = transport
|
|
t.Cleanup(func() {
|
|
transport.CloseIdleConnections()
|
|
})
|
|
}
|