Files
coder/cli/schedule_test.go
T
Benjamin Peinhardt 02fc173df4 fix: fix flake due to two time.Now() calls (#19450)
fixes https://github.com/coder/internal/issues/559

This test is looking to see that after calling `coder schedule extend
<workspace> 10h`, the scheduled stop time of the workspace is updated
appropriately (or at least that the information printed to the terminal
indicates that).

By using two `time.Now()` calls for the current time and the expected
time, there was the possibility that the second call just barely crossed
over the hour mark. This is shown in the error message when the test
would flake: `wanted "2025-04-07T22:"; got " 2025-04-07T23:00:00+05:30
\r\n"` (the 00:00 letting us know we just barely crossed the hour).

Using the same time object to construct the expected time should fix the
issue.
2025-08-20 12:12:02 -05:00

376 lines
14 KiB
Go

package cli_test
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/coderd/util/tz"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
// setupTestSchedule creates 4 workspaces:
// 1. a-owner-ws1: owned by owner, has both autostart and autostop enabled.
// 2. b-owner-ws2: owned by owner, has only autostart enabled.
// 3. c-member-ws3: owned by member, has only autostop enabled.
// 4. d-member-ws4: owned by member, has neither autostart nor autostop enabled.
// It returns the owner and member clients, the database, and the workspaces.
// The workspaces are returned in the same order as they are created.
func setupTestSchedule(t *testing.T, sched *cron.Schedule) (ownerClient, memberClient *codersdk.Client, db database.Store, ws []codersdk.Workspace) {
t.Helper()
ownerClient, db = coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, memberUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
r.Username = "testuser2" // ensure deterministic ordering
})
_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
Name: "a-owner",
OwnerID: owner.UserID,
OrganizationID: owner.OrganizationID,
AutostartSchedule: sql.NullString{String: sched.String(), Valid: true},
Ttl: sql.NullInt64{Int64: 8 * time.Hour.Nanoseconds(), Valid: true},
}).WithAgent().Do()
_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
Name: "b-owner",
OwnerID: owner.UserID,
OrganizationID: owner.OrganizationID,
AutostartSchedule: sql.NullString{String: sched.String(), Valid: true},
}).WithAgent().Do()
_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
Name: "c-member",
OwnerID: memberUser.ID,
OrganizationID: owner.OrganizationID,
Ttl: sql.NullInt64{Int64: 8 * time.Hour.Nanoseconds(), Valid: true},
}).WithAgent().Do()
_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
Name: "d-member",
OwnerID: memberUser.ID,
OrganizationID: owner.OrganizationID,
}).WithAgent().Do()
// Need this for LatestBuild.Deadline
resp, err := ownerClient.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
require.NoError(t, err)
require.Len(t, resp.Workspaces, 4)
// Ensure same order as in CLI output
ws = resp.Workspaces
sort.Slice(ws, func(i, j int) bool {
a := ws[i].OwnerName + "/" + ws[i].Name
b := ws[j].OwnerName + "/" + ws[j].Name
return a < b
})
return ownerClient, memberClient, db, ws
}
//nolint:paralleltest // t.Setenv
func TestScheduleShow(t *testing.T) {
// Given
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
t.Setenv("TZ", "Asia/Kolkata")
loc, err := tz.TimezoneIANA()
require.NoError(t, err)
require.Equal(t, "Asia/Kolkata", loc.String())
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
require.NoError(t, err, "invalid schedule")
ownerClient, memberClient, _, ws := setupTestSchedule(t, sched)
now := time.Now()
t.Run("OwnerNoArgs", func(t *testing.T) {
// When: owner specifies no args
inv, root := clitest.New(t, "schedule", "show")
//nolint:gocritic // Testing that owner user sees all
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: they should see their own workspaces.
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: b-owner-ws2 has only autostart enabled.
pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})
t.Run("OwnerAll", func(t *testing.T) {
// When: owner lists all workspaces
inv, root := clitest.New(t, "schedule", "show", "--all")
//nolint:gocritic // Testing that owner user sees all
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: they should see all workspaces
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: b-owner-ws2 has only autostart enabled.
pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
// 3rd workspace: c-member-ws3 has only autostop enabled.
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})
t.Run("OwnerSearchByName", func(t *testing.T) {
// When: owner specifies a search query
inv, root := clitest.New(t, "schedule", "show", "--search", "name:"+ws[1].Name)
//nolint:gocritic // Testing that owner user sees all
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: they should see workspaces matching that query
// 2nd workspace: b-owner-ws2 has only autostart enabled.
pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})
t.Run("OwnerOneArg", func(t *testing.T) {
// When: owner asks for a specific workspace by name
inv, root := clitest.New(t, "schedule", "show", ws[2].OwnerName+"/"+ws[2].Name)
//nolint:gocritic // Testing that owner user sees all
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: they should see that workspace
// 3rd workspace: c-member-ws3 has only autostop enabled.
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
})
t.Run("MemberNoArgs", func(t *testing.T) {
// When: a member specifies no args
inv, root := clitest.New(t, "schedule", "show")
clitest.SetupConfig(t, memberClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: they should see their own workspaces
// 1st workspace: c-member-ws3 has only autostop enabled.
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})
t.Run("MemberAll", func(t *testing.T) {
// When: a member lists all workspaces
inv, root := clitest.New(t, "schedule", "show", "--all")
clitest.SetupConfig(t, memberClient, root)
pty := ptytest.New(t).Attach(inv)
ctx := testutil.Context(t, testutil.WaitShort)
errC := make(chan error)
go func() {
errC <- inv.WithContext(ctx).Run()
}()
require.NoError(t, <-errC)
// Then: they should only see their own
// 1st workspace: c-member-ws3 has only autostop enabled.
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})
t.Run("JSON", func(t *testing.T) {
// When: owner lists all workspaces in JSON format
inv, root := clitest.New(t, "schedule", "show", "--all", "--output", "json")
var buf bytes.Buffer
inv.Stdout = &buf
clitest.SetupConfig(t, ownerClient, root)
ctx := testutil.Context(t, testutil.WaitShort)
errC := make(chan error)
go func() {
errC <- inv.WithContext(ctx).Run()
}()
assert.NoError(t, <-errC)
// Then: they should see all workspace schedules in JSON format
var parsed []map[string]string
require.NoError(t, json.Unmarshal(buf.Bytes(), &parsed))
require.Len(t, parsed, 4)
// Ensure same order as in CLI output
sort.Slice(parsed, func(i, j int) bool {
a := parsed[i]["workspace"]
b := parsed[j]["workspace"]
return a < b
})
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
assert.Equal(t, ws[0].OwnerName+"/"+ws[0].Name, parsed[0]["workspace"])
assert.Equal(t, sched.Humanize(), parsed[0]["starts_at"])
assert.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), parsed[0]["starts_next"])
assert.Equal(t, "8h", parsed[0]["stops_after"])
assert.Equal(t, ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339), parsed[0]["stops_next"])
// 2nd workspace: b-owner-ws2 has only autostart enabled.
assert.Equal(t, ws[1].OwnerName+"/"+ws[1].Name, parsed[1]["workspace"])
assert.Equal(t, sched.Humanize(), parsed[1]["starts_at"])
assert.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), parsed[1]["starts_next"])
assert.Empty(t, parsed[1]["stops_after"])
assert.Empty(t, parsed[1]["stops_next"])
// 3rd workspace: c-member-ws3 has only autostop enabled.
assert.Equal(t, ws[2].OwnerName+"/"+ws[2].Name, parsed[2]["workspace"])
assert.Empty(t, parsed[2]["starts_at"])
assert.Empty(t, parsed[2]["starts_next"])
assert.Equal(t, "8h", parsed[2]["stops_after"])
assert.Equal(t, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339), parsed[2]["stops_next"])
// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
assert.Equal(t, ws[3].OwnerName+"/"+ws[3].Name, parsed[3]["workspace"])
assert.Empty(t, parsed[3]["starts_at"])
assert.Empty(t, parsed[3]["starts_next"])
assert.Empty(t, parsed[3]["stops_after"])
})
}
//nolint:paralleltest // t.Setenv
func TestScheduleModify(t *testing.T) {
// Given
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
t.Setenv("TZ", "Asia/Kolkata")
loc, err := tz.TimezoneIANA()
require.NoError(t, err)
require.Equal(t, "Asia/Kolkata", loc.String())
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
require.NoError(t, err, "invalid schedule")
ownerClient, _, _, ws := setupTestSchedule(t, sched)
now := time.Now()
t.Run("SetStart", func(t *testing.T) {
// When: we set the start schedule
inv, root := clitest.New(t,
"schedule", "start", ws[3].OwnerName+"/"+ws[3].Name, "7:30AM", "Mon-Fri", "Europe/Dublin",
)
//nolint:gocritic // this workspace is not owned by the same user
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})
t.Run("SetStop", func(t *testing.T) {
// When: we set the stop schedule
inv, root := clitest.New(t,
"schedule", "stop", ws[2].OwnerName+"/"+ws[2].Name, "8h30m",
)
//nolint:gocritic // this workspace is not owned by the same user
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h30m")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
})
t.Run("UnsetStart", func(t *testing.T) {
// When: we unset the start schedule
inv, root := clitest.New(t,
"schedule", "start", ws[1].OwnerName+"/"+ws[1].Name, "manual",
)
//nolint:gocritic // this workspace is owned by owner
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
})
t.Run("UnsetStop", func(t *testing.T) {
// When: we unset the stop schedule
inv, root := clitest.New(t,
"schedule", "stop", ws[0].OwnerName+"/"+ws[0].Name, "manual",
)
//nolint:gocritic // this workspace is owned by owner
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
})
}
//nolint:paralleltest // t.Setenv
func TestScheduleOverride(t *testing.T) {
tests := []struct {
command string
}{
{command: "extend"},
// test for backwards compatibility
{command: "override-stop"},
}
for _, tt := range tests {
t.Run(tt.command, func(t *testing.T) {
// Given
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
t.Setenv("TZ", "Asia/Kolkata")
loc, err := tz.TimezoneIANA()
require.NoError(t, err)
require.Equal(t, "Asia/Kolkata", loc.String())
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
require.NoError(t, err, "invalid schedule")
ownerClient, _, _, ws := setupTestSchedule(t, sched)
now := time.Now()
// To avoid the likelihood of time-related flakes, only matching up to the hour.
expectedDeadline := now.In(loc).Add(10 * time.Hour).Format("2006-01-02T15:")
// When: we override the stop schedule
inv, root := clitest.New(t,
"schedule", tt.command, ws[0].OwnerName+"/"+ws[0].Name, "10h",
)
clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())
// Then: the updated schedule should be shown
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(expectedDeadline)
})
}
}