Thanks to visit codestin.com
Credit goes to github.com

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 <[email protected]> * Add test for listing project parameters * Cache npm dependencies with setup node * Remove windows npm cache key Co-authored-by: Bryan <[email protected]>
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-
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-
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+
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)