chore: move workspace apps tests to new package (#7025)

* chore: move workspace apps tests to new package
* chore: move reconnecting pty to apptest package
This commit is contained in:
Dean Sheather 2023-04-07 07:35:27 +10:00 committed by GitHub
parent b2892c3d17
commit 5398150c25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1609 additions and 1530 deletions

View File

@ -9,10 +9,14 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/testutil"
)
@ -29,7 +33,6 @@ func TestWorkspaceActivityBump(t *testing.T) {
}
client = coderdtest.New(t, &coderdtest.Options{
AppHostname: proxyTestSubdomainRaw,
IncludeProvisionerDaemon: true,
// Agent stats trigger the activity bump, so we want to report
// very frequently in tests.
@ -47,9 +50,45 @@ func TestWorkspaceActivityBump(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
ttlMillis := int64(ttl / time.Millisecond)
workspace = createWorkspaceWithApps(t, client, user.OrganizationID, "", 1234, func(cwr *codersdk.CreateWorkspaceRequest) {
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "agent",
Auth: &proto.Agent_Token{
Token: agentToken,
},
}},
}},
},
},
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTLMillis = &ttlMillis
})
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(agentToken)
agentCloser := agent.New(agent.Options{
Client: agentClient,
Logger: slogtest.Make(t, nil).Named("agent"),
})
t.Cleanup(func() {
_ = agentCloser.Close()
})
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
// Sanity-check that deadline is near.
workspace, err := client.Workspace(ctx, workspace.ID)

View File

@ -14,23 +14,6 @@ import (
// While running tests in parallel, the web server seems to be overloaded and responds with HTTP 502.
// require.Eventually expects correct HTTP responses.
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 require.TestingT, client *codersdk.Client, method, path string, body interface{}, opts ...codersdk.RequestOption) (*http.Response, error) {
var resp *http.Response
var err error

View File

@ -1,9 +1,7 @@
package coderd_test
import (
"bufio"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
@ -446,88 +444,6 @@ func TestWorkspaceAgentTailnet(t *testing.T) {
require.Equal(t, "test", strings.TrimSpace(string(output)))
}
func TestWorkspaceAgentPTY(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
// This might be our implementation, or ConPTY itself.
// It's difficult to find extensive tests for it, so
// it seems like it could be either.
t.Skip("ConPTY appears to be inconsistent on Windows.")
}
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
agentCloser := agent.New(agent.Options{
Client: agentClient,
Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug),
})
defer func() {
_ = agentCloser.Close()
}()
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
conn, err := client.WorkspaceAgentReconnectingPTY(ctx, resources[0].Agents[0].ID, uuid.New(), 80, 80, "/bin/bash")
require.NoError(t, err)
defer conn.Close()
// First attempt to resize the TTY.
// The websocket will close if it fails!
data, err := json.Marshal(codersdk.ReconnectingPTYRequest{
Height: 250,
Width: 250,
})
require.NoError(t, err)
_, err = conn.Write(data)
require.NoError(t, err)
bufRead := bufio.NewReader(conn)
// Brief pause to reduce the likelihood that we send keystrokes while
// the shell is simultaneously sending a prompt.
time.Sleep(100 * time.Millisecond)
data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
Data: "echo test\r\n",
})
require.NoError(t, err)
_, err = conn.Write(data)
require.NoError(t, err)
expectLine := func(matcher func(string) bool) {
for {
line, err := bufRead.ReadString('\n')
require.NoError(t, err)
if matcher(line) {
break
}
}
}
matchEchoCommand := func(line string) bool {
return strings.Contains(line, "echo test")
}
matchEchoOutput := func(line string) bool {
return strings.Contains(line, "test") && !strings.Contains(line, "echo")
}
expectLine(matchEchoCommand)
expectLine(matchEchoOutput)
}
func TestWorkspaceAgentListeningPorts(t *testing.T) {
t.Parallel()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,401 @@
package apptest
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/testutil"
)
const (
proxyTestAgentName = "agent-name"
proxyTestAppNameFake = "test-app-fake"
proxyTestAppNameOwner = "test-app-owner"
proxyTestAppNameAuthenticated = "test-app-authenticated"
proxyTestAppNamePublic = "test-app-public"
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 {
AppHost string
DisablePathApps bool
DisableSubdomainApps bool
DangerousAllowPathAppSharing bool
DangerousAllowPathAppSiteOwnerAccess bool
// The following fields are only used by setupProxyTestWithFactory.
noWorkspace bool
port uint16
}
// Deployment is a license-agnostic deployment with all the fields that apps
// tests need.
type Deployment struct {
Options *DeploymentOptions
// Client should be logged in as the admin user.
Client *codersdk.Client
FirstUser codersdk.CreateFirstUserResponse
PathAppBaseURL *url.URL
}
// 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
Query string
}
// AppDetails are the full test details returned from setupProxyTestWithFactory.
type AppDetails 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
FakeApp App
OwnerApp App
AuthenticatedApp App
PublicApp App
PortApp App
}
// PathAppURL returns the URL for the given path app.
func (d *AppDetails) 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 *AppDetails) SubdomainAppURL(app App) *url.URL {
if d.Options.DisableSubdomainApps || d.Options.AppHost == "" {
panic("subdomain apps are disabled")
}
host := fmt.Sprintf("%s--%s--%s--%s", app.AppSlugOrPort, app.AgentName, app.WorkspaceName, app.Username)
u := *d.PathAppBaseURL
u.Host = strings.Replace(d.Options.AppHost, "*", host, 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) *AppDetails {
if opts == nil {
opts = &DeploymentOptions{}
}
if opts.AppHost == "" {
opts.AppHost = proxyTestSubdomainRaw
}
if opts.DisableSubdomainApps {
opts.AppHost = ""
}
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.Client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
forceURLTransport(t, deployment.Client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancel()
me, err := deployment.Client.User(ctx, codersdk.Me)
require.NoError(t, err)
if opts.noWorkspace {
return &AppDetails{
Deployment: deployment,
Me: me,
}
}
if opts.port == 0 {
opts.port = appServer(t)
}
workspace, agnt := createWorkspaceWithApps(t, deployment.Client, deployment.FirstUser.OrganizationID, me, opts.AppHost, opts.port)
return &AppDetails{
Deployment: deployment,
Me: me,
Workspace: &workspace,
Agent: &agnt,
AppPort: opts.port,
FakeApp: App{
Username: me.Username,
WorkspaceName: workspace.Name,
AgentName: agnt.Name,
AppSlugOrPort: proxyTestAppNameFake,
},
OwnerApp: App{
Username: me.Username,
WorkspaceName: workspace.Name,
AgentName: agnt.Name,
AppSlugOrPort: proxyTestAppNameOwner,
Query: proxyTestAppQuery,
},
AuthenticatedApp: App{
Username: me.Username,
WorkspaceName: workspace.Name,
AgentName: agnt.Name,
AppSlugOrPort: proxyTestAppNameAuthenticated,
Query: proxyTestAppQuery,
},
PublicApp: App{
Username: me.Username,
WorkspaceName: workspace.Name,
AgentName: agnt.Name,
AppSlugOrPort: proxyTestAppNamePublic,
Query: proxyTestAppQuery,
},
PortApp: App{
Username: me.Username,
WorkspaceName: workspace.Name,
AgentName: agnt.Name,
AppSlugOrPort: strconv.Itoa(int(opts.port)),
},
}
}
func appServer(t *testing.T) uint16 {
// Start a listener on a random port greater than the minimum app port.
var (
ln net.Listener
tcpAddr *net.TCPAddr
)
for i := 0; i < 10; i++ {
var err error
// #nosec
ln, err = net.Listen("tcp", ":0")
require.NoError(t, err)
var ok bool
tcpAddr, ok = ln.Addr().(*net.TCPAddr)
require.True(t, ok)
if tcpAddr.Port < codersdk.WorkspaceAgentMinimumListeningPort {
_ = ln.Close()
time.Sleep(20 * time.Millisecond)
continue
}
}
server := http.Server{
ReadHeaderTimeout: time.Minute,
Handler: 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.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(proxyTestAppBody))
}),
}
t.Cleanup(func() {
_ = server.Close()
_ = ln.Close()
})
go func() {
_ = server.Serve(ln)
}()
return uint16(tcpAddr.Port)
}
func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.UUID, me codersdk.User, appHost string, port uint16, workspaceMutators ...func(*codersdk.CreateWorkspaceRequest)) (codersdk.Workspace, codersdk.WorkspaceAgent) {
authToken := uuid.NewString()
appURL := fmt.Sprintf("http://127.0.0.1:%d?%s", port, proxyTestAppQuery)
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: proxyTestAgentName,
Auth: &proto.Agent_Token{
Token: authToken,
},
Apps: []*proto.App{
{
Slug: proxyTestAppNameFake,
DisplayName: proxyTestAppNameFake,
SharingLevel: proto.AppSharingLevel_OWNER,
// Hopefully this IP and port doesn't exist.
Url: "http://127.1.0.1:65535",
},
{
Slug: proxyTestAppNameOwner,
DisplayName: proxyTestAppNameOwner,
SharingLevel: proto.AppSharingLevel_OWNER,
Url: appURL,
},
{
Slug: proxyTestAppNameAuthenticated,
DisplayName: proxyTestAppNameAuthenticated,
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
Url: appURL,
},
{
Slug: proxyTestAppNamePublic,
DisplayName: proxyTestAppNamePublic,
SharingLevel: proto.AppSharingLevel_PUBLIC,
Url: appURL,
},
},
}},
}},
},
},
}},
})
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID, workspaceMutators...)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
if appHost != "" {
manifest, err := agentClient.Manifest(context.Background())
require.NoError(t, err)
proxyURL := fmt.Sprintf(
"http://{{port}}--%s--%s--%s%s",
proxyTestAgentName,
workspace.Name,
me.Username,
strings.ReplaceAll(appHost, "*", ""),
)
if client.URL.Port() != "" {
proxyURL += fmt.Sprintf(":%s", client.URL.Port())
}
require.Equal(t, proxyURL, manifest.VSCodePortProxyURI)
}
agentCloser := agent.New(agent.Options{
Client: agentClient,
Logger: slogtest.Make(t, nil).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 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 require.TestingT, client *codersdk.Client, method, urlOrPath string, body interface{}, opts ...codersdk.RequestOption) (*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.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 URL 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()
})
}

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,8 @@ realpath() {
}
# We have to define realpath before these otherwise it fails on Mac's bash.
SCRIPT_DIR="$(realpath "$(dirname "${BASH_SOURCE[1]}")")"
SCRIPT="${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}"
SCRIPT_DIR="$(realpath "$(dirname "$SCRIPT")")"
PROJECT_ROOT="$(cd "$SCRIPT_DIR" && realpath "$(git rev-parse --show-toplevel)")"
# pushd is a silent alternative to the real pushd shell command.