Skip to content

Commit 1796dc6

Browse files
chore: Add test helpers to improve coverage (#166)
* chore: Rename ProjectHistory to ProjectVersion Version more accurately represents version storage. This forks from the WorkspaceHistory name, but I think it's easier to understand Workspace history. * Rename files * Standardize tests a bit more * Remove Server struct from coderdtest * Improve test coverage for workspace history * Fix linting errors * Fix coderd test leak * Fix coderd test leak * Improve workspace history logs * Standardize test structure for codersdk * Fix linting errors * Fix WebSocket compression * Update coderd/workspaces.go Co-authored-by: Bryan <bryan@coder.com> * Add test for listing project parameters * Cache npm dependencies with setup node * Remove windows npm cache key Co-authored-by: Bryan <bryan@coder.com>
1 parent f19770b commit 1796dc6

38 files changed

+1381
-972
lines changed

.github/workflows/coder.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,13 @@ jobs:
165165
-covermode=atomic -coverprofile="gotests.coverage" -timeout=3m
166166
-count=1 -race -parallel=2
167167

168-
- uses: actions/setup-node@v2
168+
- name: Setup Node for DataDog CLI
169+
uses: actions/setup-node@v2
169170
if: always() && github.actor != 'dependabot[bot]'
170171
with:
171172
node-version: "14"
172173

173-
- name: Cache DataDog CI
174+
- name: Cache DataDog CLI
174175
if: always() && github.actor != 'dependabot[bot]'
175176
uses: actions/cache@v2
176177
with:

.golangci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ linters-settings:
100100
# - whyNoLint
101101
# - wrapperFunc
102102
# - yodaStyleExpr
103+
settings:
104+
ruleguard:
105+
failOn: all
106+
rules: rules.go
103107

104108
goimports:
105109
local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder

coderd/coderd.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ func New(options *Options) http.Handler {
8686
r.Get("/", api.workspaces)
8787
r.Route("/{user}", func(r chi.Router) {
8888
r.Use(httpmw.ExtractUserParam(options.Database))
89-
r.Get("/", api.workspaces)
9089
r.Post("/", api.postWorkspaceByUser)
9190
r.Route("/{workspace}", func(r chi.Router) {
9291
r.Use(httpmw.ExtractWorkspaceParam(options.Database))

coderd/coderd_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package coderd_test
2+
3+
import (
4+
"testing"
5+
6+
"go.uber.org/goleak"
7+
)
8+
9+
func TestMain(m *testing.M) {
10+
goleak.VerifyTestMain(m)
11+
}

coderd/coderdtest/coderdtest.go

Lines changed: 117 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import (
77
"net/http/httptest"
88
"net/url"
99
"os"
10+
"strings"
1011
"testing"
1112
"time"
1213

14+
"github.com/google/uuid"
15+
"github.com/moby/moby/pkg/namesgenerator"
1316
"github.com/stretchr/testify/require"
1417

1518
"cdr.dev/slog"
1619
"cdr.dev/slog/sloggers/slogtest"
1720
"github.com/coder/coder/coderd"
1821
"github.com/coder/coder/codersdk"
19-
"github.com/coder/coder/cryptorand"
2022
"github.com/coder/coder/database"
2123
"github.com/coder/coder/database/databasefake"
2224
"github.com/coder/coder/database/postgres"
@@ -26,47 +28,49 @@ import (
2628
"github.com/coder/coder/provisionersdk/proto"
2729
)
2830

29-
// Server represents a test instance of coderd.
30-
// The database is intentionally omitted from
31-
// this struct to promote data being exposed via
32-
// the API.
33-
type Server struct {
34-
Client *codersdk.Client
35-
URL *url.URL
36-
}
37-
38-
// RandomInitialUser generates a random initial user and authenticates
39-
// it with the client on the Server struct.
40-
func (s *Server) RandomInitialUser(t *testing.T) coderd.CreateInitialUserRequest {
41-
username, err := cryptorand.String(12)
42-
require.NoError(t, err)
43-
password, err := cryptorand.String(12)
44-
require.NoError(t, err)
45-
organization, err := cryptorand.String(12)
46-
require.NoError(t, err)
31+
// New constructs a new coderd test instance. This returned Server
32+
// should contain no side-effects.
33+
func New(t *testing.T) *codersdk.Client {
34+
// This can be hotswapped for a live database instance.
35+
db := databasefake.New()
36+
pubsub := database.NewPubsubInMemory()
37+
if os.Getenv("DB") != "" {
38+
connectionURL, close, err := postgres.Open()
39+
require.NoError(t, err)
40+
t.Cleanup(close)
41+
sqlDB, err := sql.Open("postgres", connectionURL)
42+
require.NoError(t, err)
43+
t.Cleanup(func() {
44+
_ = sqlDB.Close()
45+
})
46+
err = database.Migrate(sqlDB)
47+
require.NoError(t, err)
48+
db = database.New(sqlDB)
4749

48-
req := coderd.CreateInitialUserRequest{
49-
Email: "testuser@coder.com",
50-
Username: username,
51-
Password: password,
52-
Organization: organization,
50+
pubsub, err = database.NewPubsub(context.Background(), sqlDB, connectionURL)
51+
require.NoError(t, err)
52+
t.Cleanup(func() {
53+
_ = pubsub.Close()
54+
})
5355
}
54-
_, err = s.Client.CreateInitialUser(context.Background(), req)
55-
require.NoError(t, err)
5656

57-
login, err := s.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
58-
Email: "testuser@coder.com",
59-
Password: password,
57+
handler := coderd.New(&coderd.Options{
58+
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
59+
Database: db,
60+
Pubsub: pubsub,
6061
})
62+
srv := httptest.NewServer(handler)
63+
serverURL, err := url.Parse(srv.URL)
6164
require.NoError(t, err)
62-
err = s.Client.SetSessionToken(login.SessionToken)
63-
require.NoError(t, err)
64-
return req
65+
t.Cleanup(srv.Close)
66+
67+
return codersdk.New(serverURL)
6568
}
6669

67-
// AddProvisionerd launches a new provisionerd instance with the
68-
// test provisioner registered.
69-
func (s *Server) AddProvisionerd(t *testing.T) io.Closer {
70+
// NewProvisionerDaemon launches a provisionerd instance configured to work
71+
// well with coderd testing. It registers the "echo" provisioner for
72+
// quick testing.
73+
func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
7074
echoClient, echoServer := provisionersdk.TransportPipe()
7175
ctx, cancelFunc := context.WithCancel(context.Background())
7276
t.Cleanup(func() {
@@ -81,7 +85,7 @@ func (s *Server) AddProvisionerd(t *testing.T) io.Closer {
8185
require.NoError(t, err)
8286
}()
8387

84-
closer := provisionerd.New(s.Client.ProvisionerDaemonClient, &provisionerd.Options{
88+
closer := provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{
8589
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
8690
PollInterval: 50 * time.Millisecond,
8791
UpdateInterval: 50 * time.Millisecond,
@@ -96,44 +100,87 @@ func (s *Server) AddProvisionerd(t *testing.T) io.Closer {
96100
return closer
97101
}
98102

99-
// New constructs a new coderd test instance. This returned Server
100-
// should contain no side-effects.
101-
func New(t *testing.T) Server {
102-
// This can be hotswapped for a live database instance.
103-
db := databasefake.New()
104-
pubsub := database.NewPubsubInMemory()
105-
if os.Getenv("DB") != "" {
106-
connectionURL, close, err := postgres.Open()
107-
require.NoError(t, err)
108-
t.Cleanup(close)
109-
sqlDB, err := sql.Open("postgres", connectionURL)
110-
require.NoError(t, err)
111-
t.Cleanup(func() {
112-
_ = sqlDB.Close()
113-
})
114-
err = database.Migrate(sqlDB)
115-
require.NoError(t, err)
116-
db = database.New(sqlDB)
103+
// CreateInitialUser creates a user with preset credentials and authenticates
104+
// with the passed in codersdk client.
105+
func CreateInitialUser(t *testing.T, client *codersdk.Client) coderd.CreateInitialUserRequest {
106+
req := coderd.CreateInitialUserRequest{
107+
Email: "testuser@coder.com",
108+
Username: "testuser",
109+
Password: "testpass",
110+
Organization: "testorg",
111+
}
112+
_, err := client.CreateInitialUser(context.Background(), req)
113+
require.NoError(t, err)
117114

118-
pubsub, err = database.NewPubsub(context.Background(), sqlDB, connectionURL)
115+
login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
116+
Email: req.Email,
117+
Password: req.Password,
118+
})
119+
require.NoError(t, err)
120+
err = client.SetSessionToken(login.SessionToken)
121+
require.NoError(t, err)
122+
return req
123+
}
124+
125+
// CreateProject creates a project with the "echo" provisioner for
126+
// compatibility with testing. The name assigned is randomly generated.
127+
func CreateProject(t *testing.T, client *codersdk.Client, organization string) coderd.Project {
128+
project, err := client.CreateProject(context.Background(), organization, coderd.CreateProjectRequest{
129+
Name: randomUsername(),
130+
Provisioner: database.ProvisionerTypeEcho,
131+
})
132+
require.NoError(t, err)
133+
return project
134+
}
135+
136+
// CreateProjectVersion creates a project version for the "echo" provisioner
137+
// for compatibility with testing.
138+
func CreateProjectVersion(t *testing.T, client *codersdk.Client, organization, project string, responses *echo.Responses) coderd.ProjectVersion {
139+
data, err := echo.Tar(responses)
140+
require.NoError(t, err)
141+
version, err := client.CreateProjectVersion(context.Background(), organization, project, coderd.CreateProjectVersionRequest{
142+
StorageMethod: database.ProjectStorageMethodInlineArchive,
143+
StorageSource: data,
144+
})
145+
require.NoError(t, err)
146+
return version
147+
}
148+
149+
// AwaitProjectVersionImported awaits for the project import job to reach completed status.
150+
func AwaitProjectVersionImported(t *testing.T, client *codersdk.Client, organization, project, version string) coderd.ProjectVersion {
151+
var projectVersion coderd.ProjectVersion
152+
require.Eventually(t, func() bool {
153+
var err error
154+
projectVersion, err = client.ProjectVersion(context.Background(), organization, project, version)
119155
require.NoError(t, err)
120-
t.Cleanup(func() {
121-
_ = pubsub.Close()
122-
})
123-
}
156+
return projectVersion.Import.Status.Completed()
157+
}, 3*time.Second, 25*time.Millisecond)
158+
return projectVersion
159+
}
124160

125-
handler := coderd.New(&coderd.Options{
126-
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
127-
Database: db,
128-
Pubsub: pubsub,
161+
// CreateWorkspace creates a workspace for the user and project provided.
162+
// A random name is generated for it.
163+
func CreateWorkspace(t *testing.T, client *codersdk.Client, user string, projectID uuid.UUID) coderd.Workspace {
164+
workspace, err := client.CreateWorkspace(context.Background(), user, coderd.CreateWorkspaceRequest{
165+
ProjectID: projectID,
166+
Name: randomUsername(),
129167
})
130-
srv := httptest.NewServer(handler)
131-
serverURL, err := url.Parse(srv.URL)
132168
require.NoError(t, err)
133-
t.Cleanup(srv.Close)
169+
return workspace
170+
}
134171

135-
return Server{
136-
Client: codersdk.New(serverURL),
137-
URL: serverURL,
138-
}
172+
// AwaitWorkspaceHistoryProvisioned awaits for the workspace provision job to reach completed status.
173+
func AwaitWorkspaceHistoryProvisioned(t *testing.T, client *codersdk.Client, user, workspace, history string) coderd.WorkspaceHistory {
174+
var workspaceHistory coderd.WorkspaceHistory
175+
require.Eventually(t, func() bool {
176+
var err error
177+
workspaceHistory, err = client.WorkspaceHistory(context.Background(), user, workspace, history)
178+
require.NoError(t, err)
179+
return workspaceHistory.Provision.Status.Completed()
180+
}, 3*time.Second, 25*time.Millisecond)
181+
return workspaceHistory
182+
}
183+
184+
func randomUsername() string {
185+
return strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-")
139186
}

coderd/coderdtest/coderdtest_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package coderdtest_test
22

33
import (
4+
"context"
45
"testing"
56

67
"go.uber.org/goleak"
78

9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/coderd"
812
"github.com/coder/coder/coderd/coderdtest"
13+
"github.com/coder/coder/database"
914
)
1015

1116
func TestMain(m *testing.M) {
@@ -14,7 +19,18 @@ func TestMain(m *testing.M) {
1419

1520
func TestNew(t *testing.T) {
1621
t.Parallel()
17-
server := coderdtest.New(t)
18-
_ = server.RandomInitialUser(t)
19-
_ = server.AddProvisionerd(t)
22+
client := coderdtest.New(t)
23+
user := coderdtest.CreateInitialUser(t, client)
24+
closer := coderdtest.NewProvisionerDaemon(t, client)
25+
project := coderdtest.CreateProject(t, client, user.Organization)
26+
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
27+
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
28+
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
29+
history, err := client.CreateWorkspaceHistory(context.Background(), "me", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
30+
ProjectVersionID: version.ID,
31+
Transition: database.WorkspaceTransitionCreate,
32+
})
33+
require.NoError(t, err)
34+
coderdtest.AwaitWorkspaceHistoryProvisioned(t, client, "me", workspace.Name, history.Name)
35+
closer.Close()
2036
}

coderd/projects.go

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) {
4949
})
5050
return
5151
}
52+
if projects == nil {
53+
projects = []database.Project{}
54+
}
5255
render.Status(r, http.StatusOK)
5356
render.JSON(rw, r, projects)
5457
}
@@ -66,6 +69,9 @@ func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request)
6669
})
6770
return
6871
}
72+
if projects == nil {
73+
projects = []database.Project{}
74+
}
6975
render.Status(r, http.StatusOK)
7076
render.JSON(rw, r, projects)
7177
}
@@ -124,32 +130,6 @@ func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) {
124130
render.JSON(rw, r, project)
125131
}
126132

127-
// Returns all workspaces for a specific project.
128-
func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) {
129-
apiKey := httpmw.APIKey(r)
130-
project := httpmw.ProjectParam(r)
131-
workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{
132-
OwnerID: apiKey.UserID,
133-
ProjectID: project.ID,
134-
})
135-
if errors.Is(err, sql.ErrNoRows) {
136-
err = nil
137-
}
138-
if err != nil {
139-
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
140-
Message: fmt.Sprintf("get workspaces: %s", err),
141-
})
142-
return
143-
}
144-
145-
apiWorkspaces := make([]Workspace, 0, len(workspaces))
146-
for _, workspace := range workspaces {
147-
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
148-
}
149-
render.Status(r, http.StatusOK)
150-
render.JSON(rw, r, apiWorkspaces)
151-
}
152-
153133
// Creates parameters for a project.
154134
// This should validate the calling user has permissions!
155135
func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy