Skip to content

Commit d9afcb6

Browse files
committed
update unit tests
1 parent 00fe901 commit d9afcb6

File tree

3 files changed

+232
-79
lines changed

3 files changed

+232
-79
lines changed

cli/schedule.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"fmt"
55
"io"
6+
"strings"
67
"time"
78

89
"golang.org/x/xerrors"
@@ -104,7 +105,17 @@ func (r *RootCmd) scheduleShow() *clibase.Cmd {
104105
// This will clobber the search query if one is passed.
105106
f := filter.Filter()
106107
if len(inv.Args) == 1 {
107-
f.FilterQuery = fmt.Sprintf("owner:me name:%s", inv.Args[0])
108+
// If the argument contains a slash, we assume it's a full owner/name reference
109+
if strings.Contains(inv.Args[0], "/") {
110+
_, workspaceName, err := splitNamedWorkspace(inv.Args[0])
111+
if err != nil {
112+
return err
113+
}
114+
f.FilterQuery = fmt.Sprintf("name:%s", workspaceName)
115+
} else {
116+
// Otherwise, we assume it's a workspace name owned by the current user
117+
f.FilterQuery = fmt.Sprintf("owner:me name:%s", inv.Args[0])
118+
}
108119
}
109120
res, err := queryConvertWorkspaces(inv.Context(), client, f, scheduleListRowFromWorkspace)
110121
if err != nil {
@@ -300,7 +311,7 @@ func scheduleListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) s
300311
nextStartDisplay := ""
301312
if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
302313
if sched, err := cron.Weekly(*workspace.AutostartSchedule); err == nil {
303-
autostartDisplay = fmt.Sprintf("%s %s (%s)", sched.Time(), sched.DaysOfWeek(), sched.Location())
314+
autostartDisplay = sched.Humanize()
304315
nextStartDisplay = timeDisplay(sched.Next(now))
305316
}
306317
}
@@ -315,7 +326,7 @@ func scheduleListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) s
315326
}
316327
}
317328
return scheduleListRow{
318-
WorkspaceName: workspace.Name,
329+
WorkspaceName: workspace.OwnerName + "/" + workspace.Name,
319330
StartsAt: autostartDisplay,
320331
StartsNext: nextStartDisplay,
321332
StopsAfter: autostopDisplay,

cli/schedule_test.go

+204-75
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package cli_test
33
import (
44
"bytes"
55
"context"
6-
"fmt"
6+
"database/sql"
7+
"sort"
78
"strings"
89
"testing"
910
"time"
@@ -14,101 +15,229 @@ import (
1415
"github.com/coder/coder/v2/cli/clitest"
1516
"github.com/coder/coder/v2/coderd/coderdtest"
1617
"github.com/coder/coder/v2/coderd/database"
17-
"github.com/coder/coder/v2/coderd/util/ptr"
18+
"github.com/coder/coder/v2/coderd/database/dbfake"
19+
"github.com/coder/coder/v2/coderd/schedule/cron"
1820
"github.com/coder/coder/v2/codersdk"
21+
"github.com/coder/coder/v2/pty/ptytest"
22+
"github.com/coder/coder/v2/testutil"
1923
)
2024

25+
func setupTestSchedule(t *testing.T, sched *cron.Schedule) (ownerClient, memberClient *codersdk.Client, db database.Store) {
26+
ownerClient, db = coderdtest.NewWithDatabase(t, nil)
27+
owner := coderdtest.CreateFirstUser(t, ownerClient)
28+
memberClient, memberUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
29+
r.Username = "testuser2" // ensure deterministic ordering
30+
})
31+
_, _ = dbfake.WorkspaceWithAgent(t, db, database.Workspace{
32+
Name: "a-owner",
33+
OwnerID: owner.UserID,
34+
OrganizationID: owner.OrganizationID,
35+
AutostartSchedule: sql.NullString{String: sched.String(), Valid: true},
36+
Ttl: sql.NullInt64{Int64: 8 * time.Hour.Nanoseconds(), Valid: true},
37+
})
38+
_, _ = dbfake.WorkspaceWithAgent(t, db, database.Workspace{
39+
Name: "b-owner",
40+
OwnerID: owner.UserID,
41+
OrganizationID: owner.OrganizationID,
42+
AutostartSchedule: sql.NullString{String: sched.String(), Valid: true},
43+
})
44+
_, _ = dbfake.WorkspaceWithAgent(t, db, database.Workspace{
45+
Name: "c-member",
46+
OwnerID: memberUser.ID,
47+
OrganizationID: owner.OrganizationID,
48+
Ttl: sql.NullInt64{Int64: 8 * time.Hour.Nanoseconds(), Valid: true},
49+
})
50+
_, _ = dbfake.WorkspaceWithAgent(t, db, database.Workspace{
51+
Name: "d-member",
52+
OwnerID: memberUser.ID,
53+
OrganizationID: owner.OrganizationID,
54+
})
55+
56+
return ownerClient, memberClient, db
57+
}
58+
2159
func TestScheduleShow(t *testing.T) {
2260
t.Parallel()
23-
t.Run("Enabled", func(t *testing.T) {
61+
62+
// Given
63+
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
64+
require.NoError(t, err, "invalid schedule")
65+
ownerClient, memberClient, _ := setupTestSchedule(t, sched)
66+
now := time.Now()
67+
68+
// Need this for LatestBuild.Deadline
69+
ws, err := ownerClient.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
70+
require.NoError(t, err)
71+
require.Len(t, ws.Workspaces, 4)
72+
// Ensure same order as in CLI output
73+
sort.Slice(ws.Workspaces, func(i, j int) bool {
74+
a := ws.Workspaces[i].OwnerName + "/" + ws.Workspaces[i].Name
75+
b := ws.Workspaces[j].OwnerName + "/" + ws.Workspaces[j].Name
76+
return a < b
77+
})
78+
79+
t.Run("OwnerNoArgs", func(t *testing.T) {
2480
t.Parallel()
2581

26-
var (
27-
tz = "Europe/Dublin"
28-
sched = "30 7 * * 1-5"
29-
schedCron = fmt.Sprintf("CRON_TZ=%s %s", tz, sched)
30-
ttl = 8 * time.Hour
31-
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
32-
user = coderdtest.CreateFirstUser(t, client)
33-
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
34-
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
35-
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
36-
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
37-
cwr.AutostartSchedule = ptr.Ref(schedCron)
38-
cwr.TTLMillis = ptr.Ref(ttl.Milliseconds())
39-
})
40-
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
41-
cmdArgs = []string{"schedule", "show", workspace.Name}
42-
stdoutBuf = &bytes.Buffer{}
43-
)
82+
// When
83+
inv, root := clitest.New(t, "schedule", "show")
84+
//nolint:gocritic // Testing that owner user sees all
85+
clitest.SetupConfig(t, ownerClient, root)
86+
pty := ptytest.New(t).Attach(inv)
87+
ctx := testutil.Context(t, testutil.WaitShort)
88+
done := make(chan any)
89+
go func() {
90+
errC := inv.WithContext(ctx).Run()
91+
assert.NoError(t, errC)
92+
close(done)
93+
}()
94+
<-done
95+
96+
// Then
97+
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
98+
pty.ExpectMatchContext(ctx, ws.Workspaces[0].OwnerName+"/"+ws.Workspaces[0].Name)
99+
pty.ExpectMatchContext(ctx, sched.Humanize())
100+
pty.ExpectMatchContext(ctx, sched.Next(now).Format(time.RFC3339))
101+
pty.ExpectMatchContext(ctx, "8h")
102+
pty.ExpectMatch(ws.Workspaces[0].LatestBuild.Deadline.Time.Format(time.RFC3339))
103+
// 2nd workspace: b-owner-ws2 has only autostart enabled.
104+
pty.ExpectMatch(ws.Workspaces[1].OwnerName + "/" + ws.Workspaces[1].Name)
105+
pty.ExpectMatch(sched.Humanize())
106+
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
107+
})
44108

45-
inv, root := clitest.New(t, cmdArgs...)
46-
clitest.SetupConfig(t, client, root)
47-
inv.Stdout = stdoutBuf
109+
t.Run("OwnerAll", func(t *testing.T) {
110+
t.Parallel()
48111

49-
err := inv.Run()
50-
require.NoError(t, err, "unexpected error")
51-
lines := strings.Split(strings.TrimSpace(stdoutBuf.String()), "\n")
52-
if assert.Len(t, lines, 4) {
53-
assert.Contains(t, lines[0], "Starts at 7:30AM Mon-Fri (Europe/Dublin)")
54-
assert.Contains(t, lines[1], "Starts next 7:30AM")
55-
// it should have either IST or GMT
56-
if !strings.Contains(lines[1], "IST") && !strings.Contains(lines[1], "GMT") {
57-
t.Error("expected either IST or GMT")
58-
}
59-
assert.Contains(t, lines[2], "Stops at 8h after start")
60-
assert.NotContains(t, lines[3], "Stops next -")
61-
}
112+
// When
113+
inv, root := clitest.New(t, "schedule", "show", "--all")
114+
//nolint:gocritic // Testing that owner user sees all
115+
clitest.SetupConfig(t, ownerClient, root)
116+
pty := ptytest.New(t).Attach(inv)
117+
ctx := testutil.Context(t, testutil.WaitShort)
118+
done := make(chan any)
119+
go func() {
120+
errC := inv.WithContext(ctx).Run()
121+
assert.NoError(t, errC)
122+
close(done)
123+
}()
124+
<-done
125+
126+
// Then
127+
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
128+
pty.ExpectMatchContext(ctx, ws.Workspaces[0].OwnerName+"/"+ws.Workspaces[0].Name)
129+
pty.ExpectMatchContext(ctx, sched.Humanize())
130+
pty.ExpectMatchContext(ctx, sched.Next(now).Format(time.RFC3339))
131+
pty.ExpectMatchContext(ctx, "8h")
132+
pty.ExpectMatch(ws.Workspaces[0].LatestBuild.Deadline.Time.Format(time.RFC3339))
133+
// 2nd workspace: b-owner-ws2 has only autostart enabled.
134+
pty.ExpectMatch(ws.Workspaces[1].OwnerName + "/" + ws.Workspaces[1].Name)
135+
pty.ExpectMatch(sched.Humanize())
136+
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
137+
// 3rd workspace: c-member-ws3 has only autostop enabled.
138+
pty.ExpectMatch(ws.Workspaces[2].OwnerName + "/" + ws.Workspaces[2].Name)
139+
pty.ExpectMatch("8h")
140+
pty.ExpectMatch(ws.Workspaces[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
141+
// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
142+
pty.ExpectMatch(ws.Workspaces[3].OwnerName + "/" + ws.Workspaces[3].Name)
62143
})
63144

64-
t.Run("Manual", func(t *testing.T) {
145+
t.Run("OwnerSearchByName", func(t *testing.T) {
65146
t.Parallel()
66147

67-
var (
68-
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
69-
user = coderdtest.CreateFirstUser(t, client)
70-
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
71-
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
72-
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
73-
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
74-
cwr.AutostartSchedule = nil
75-
cwr.TTLMillis = nil
76-
})
77-
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
78-
cmdArgs = []string{"schedule", "show", workspace.Name}
79-
stdoutBuf = &bytes.Buffer{}
80-
)
148+
// When
149+
inv, root := clitest.New(t, "schedule", "show", "--search", "name:"+ws.Workspaces[1].Name)
150+
//nolint:gocritic // Testing that owner user sees all
151+
clitest.SetupConfig(t, ownerClient, root)
152+
pty := ptytest.New(t).Attach(inv)
153+
ctx := testutil.Context(t, testutil.WaitShort)
154+
done := make(chan any)
155+
go func() {
156+
errC := inv.WithContext(ctx).Run()
157+
assert.NoError(t, errC)
158+
close(done)
159+
}()
160+
<-done
161+
162+
// Then
163+
// 2nd workspace: b-owner-ws2 has only autostart enabled.
164+
pty.ExpectMatch(ws.Workspaces[1].OwnerName + "/" + ws.Workspaces[1].Name)
165+
pty.ExpectMatch(sched.Humanize())
166+
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
167+
})
81168

82-
inv, root := clitest.New(t, cmdArgs...)
83-
clitest.SetupConfig(t, client, root)
84-
inv.Stdout = stdoutBuf
169+
t.Run("OwnerOneArg", func(t *testing.T) {
170+
t.Parallel()
85171

86-
err := inv.Run()
87-
require.NoError(t, err, "unexpected error")
88-
lines := strings.Split(strings.TrimSpace(stdoutBuf.String()), "\n")
89-
if assert.Len(t, lines, 4) {
90-
assert.Contains(t, lines[0], "Starts at manual")
91-
assert.Contains(t, lines[1], "Starts next -")
92-
assert.Contains(t, lines[2], "Stops at manual")
93-
assert.Contains(t, lines[3], "Stops next -")
94-
}
172+
// When
173+
inv, root := clitest.New(t, "schedule", "show", ws.Workspaces[2].OwnerName+"/"+ws.Workspaces[2].Name)
174+
//nolint:gocritic // Testing that owner user sees all
175+
clitest.SetupConfig(t, ownerClient, root)
176+
pty := ptytest.New(t).Attach(inv)
177+
ctx := testutil.Context(t, testutil.WaitShort)
178+
done := make(chan any)
179+
go func() {
180+
errC := inv.WithContext(ctx).Run()
181+
assert.NoError(t, errC)
182+
close(done)
183+
}()
184+
<-done
185+
186+
// Then
187+
// 3rd workspace: c-member-ws3 has only autostop enabled.
188+
pty.ExpectMatch(ws.Workspaces[2].OwnerName + "/" + ws.Workspaces[2].Name)
189+
pty.ExpectMatch("8h")
190+
pty.ExpectMatch(ws.Workspaces[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
95191
})
96192

97-
t.Run("NotFound", func(t *testing.T) {
193+
t.Run("MemberNoArgs", func(t *testing.T) {
98194
t.Parallel()
99195

100-
var (
101-
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
102-
user = coderdtest.CreateFirstUser(t, client)
103-
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
104-
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
105-
)
196+
// When
197+
inv, root := clitest.New(t, "schedule", "show")
198+
clitest.SetupConfig(t, memberClient, root)
199+
pty := ptytest.New(t).Attach(inv)
200+
ctx := testutil.Context(t, testutil.WaitShort)
201+
done := make(chan any)
202+
go func() {
203+
errC := inv.WithContext(ctx).Run()
204+
assert.NoError(t, errC)
205+
close(done)
206+
}()
207+
<-done
208+
209+
// Then
210+
// 1st workspace: c-member-ws3 has only autostop enabled.
211+
pty.ExpectMatch(ws.Workspaces[2].OwnerName + "/" + ws.Workspaces[2].Name)
212+
pty.ExpectMatch("8h")
213+
pty.ExpectMatch(ws.Workspaces[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
214+
// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
215+
pty.ExpectMatch(ws.Workspaces[3].OwnerName + "/" + ws.Workspaces[3].Name)
216+
})
106217

107-
inv, root := clitest.New(t, "schedule", "show", "doesnotexist")
108-
clitest.SetupConfig(t, client, root)
218+
t.Run("MemberAll", func(t *testing.T) {
219+
t.Parallel()
109220

110-
err := inv.Run()
111-
require.ErrorContains(t, err, "status code 404", "unexpected error")
221+
// When
222+
inv, root := clitest.New(t, "schedule", "show", "--all")
223+
clitest.SetupConfig(t, memberClient, root)
224+
pty := ptytest.New(t).Attach(inv)
225+
ctx := testutil.Context(t, testutil.WaitShort)
226+
done := make(chan any)
227+
go func() {
228+
errC := inv.WithContext(ctx).Run()
229+
assert.NoError(t, errC)
230+
close(done)
231+
}()
232+
<-done
233+
234+
// Then
235+
// 1st workspace: c-member-ws3 has only autostop enabled.
236+
pty.ExpectMatch(ws.Workspaces[2].OwnerName + "/" + ws.Workspaces[2].Name)
237+
pty.ExpectMatch("8h")
238+
pty.ExpectMatch(ws.Workspaces[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
239+
// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
240+
pty.ExpectMatch(ws.Workspaces[3].OwnerName + "/" + ws.Workspaces[3].Name)
112241
})
113242
}
114243

coderd/schedule/cron/cron.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ type Schedule struct {
115115
cronStr string
116116
}
117117

118-
// String serializes the schedule to its original human-friendly format.
118+
// String serializes the schedule to its original format.
119119
// The leading CRON_TZ is maintained.
120120
func (s Schedule) String() string {
121121
var sb strings.Builder
@@ -126,6 +126,19 @@ func (s Schedule) String() string {
126126
return sb.String()
127127
}
128128

129+
// Humanize returns a slightly more human-friendly representation of the
130+
// schedule.
131+
func (s Schedule) Humanize() string {
132+
var sb strings.Builder
133+
_, _ = sb.WriteString(s.Time())
134+
_, _ = sb.WriteString(" ")
135+
_, _ = sb.WriteString(s.DaysOfWeek())
136+
_, _ = sb.WriteString(" (")
137+
_, _ = sb.WriteString(s.Location().String())
138+
_, _ = sb.WriteString(")")
139+
return sb.String()
140+
}
141+
129142
// Location returns the IANA location for the schedule.
130143
func (s Schedule) Location() *time.Location {
131144
return s.sched.Location

0 commit comments

Comments
 (0)