mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
c650aabbef
My agent added `//nolint:testpackage` to a test file on one of my PRs. Again. This PR cleans it up across the entire repo and updates the in-repo conventions so future agents stop doing it. The repo already has a precedent for white-box tests that need to touch unexported symbols: `*_internal_test.go` (145+ existing files). The `testpackage` linter's default `skip-regexp` exempts that filename suffix, so the `//nolint:testpackage` directive is unnecessary in every case where someone reached for it. This PR renames 51 such files to `*_internal_test.go` via `git mv` so blame and history follow, and strips the dead directive from 2 files that were already correctly named (`coderd/oauth2provider/authorize_internal_test.go`, `coderd/x/chatd/advisor_internal_test.go`). `.claude/docs/TESTING.md` now documents the rule explicitly under *Test Package Naming*, which is imported into the root `AGENTS.md` via `@.claude/docs/TESTING.md`. The rule: prefer `package foo_test`; if you need internal access, rename the file to `*_internal_test.go` rather than adding a nolint directive.
497 lines
17 KiB
Go
497 lines
17 KiB
Go
package coderd
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/schedule"
|
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
// baselineTemplate returns a database.Template populated with non-default
|
|
// values for every field that resolveTemplateMetaUpdate reads. Non-default
|
|
// values let single-field tests detect when a field is being silently
|
|
// overwritten with a zero value.
|
|
func baselineTemplate() database.Template {
|
|
orgID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
|
return database.Template{
|
|
ID: uuid.MustParse("00000000-0000-0000-0000-000000000002"),
|
|
OrganizationID: orgID,
|
|
Name: "baseline",
|
|
DisplayName: "Baseline Template",
|
|
Description: "An existing description.",
|
|
Icon: "/baseline.svg",
|
|
AllowUserAutostart: false,
|
|
AllowUserAutostop: false,
|
|
AllowUserCancelWorkspaceJobs: false,
|
|
RequireActiveVersion: true,
|
|
DefaultTTL: int64(60 * 60 * 1000 * 1000 * 1000), // 1 hour in ns
|
|
ActivityBump: int64(30 * 60 * 1000 * 1000 * 1000), // 30 minutes in ns
|
|
FailureTTL: int64(120 * 60 * 1000 * 1000 * 1000), // 2 hours in ns
|
|
TimeTilDormant: int64(240 * 60 * 1000 * 1000 * 1000), // 4 hours in ns
|
|
TimeTilDormantAutoDelete: int64(480 * 60 * 1000 * 1000 * 1000), // 8 hours in ns
|
|
AutostopRequirementDaysOfWeek: 0b0000001, // Monday
|
|
AutostopRequirementWeeks: 2,
|
|
AutostartBlockDaysOfWeek: 0b1000000, // Sunday
|
|
Deprecated: "deprecated", // non-empty so the conversion is observable
|
|
MaxPortSharingLevel: database.AppSharingLevelOrganization,
|
|
UseClassicParameterFlow: true,
|
|
CorsBehavior: database.CorsBehaviorPassthru,
|
|
DisableModuleCache: true,
|
|
GroupACL: database.TemplateACL{
|
|
orgID.String(): {"read"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// baselineScheduleOpts returns schedule options matching the baseline
|
|
// template above, so that nil request fields resolve to these values.
|
|
func baselineScheduleOpts() schedule.TemplateScheduleOptions {
|
|
return schedule.TemplateScheduleOptions{
|
|
AutostopRequirement: schedule.TemplateAutostopRequirement{
|
|
DaysOfWeek: 0b0000001,
|
|
Weeks: 2,
|
|
},
|
|
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
|
DaysOfWeek: 0b1000000,
|
|
},
|
|
}
|
|
}
|
|
|
|
// baselineResolved returns the templateMetaUpdate that resolveTemplateMetaUpdate
|
|
// produces for an empty request against baselineTemplate / baselineScheduleOpts.
|
|
func baselineResolved() templateMetaUpdate {
|
|
tpl := baselineTemplate()
|
|
return templateMetaUpdate{
|
|
name: tpl.Name,
|
|
displayName: tpl.DisplayName,
|
|
description: tpl.Description,
|
|
icon: tpl.Icon,
|
|
defaultTTLMillis: tpl.DefaultTTL / 1e6,
|
|
activityBumpMillis: tpl.ActivityBump / 1e6,
|
|
failureTTLMillis: tpl.FailureTTL / 1e6,
|
|
timeTilDormantMillis: tpl.TimeTilDormant / 1e6,
|
|
timeTilDormantAutoDeleteMillis: tpl.TimeTilDormantAutoDelete / 1e6,
|
|
allowUserAutostart: tpl.AllowUserAutostart,
|
|
allowUserAutostop: tpl.AllowUserAutostop,
|
|
allowUserCancelWorkspaceJobs: tpl.AllowUserCancelWorkspaceJobs,
|
|
requireActiveVersion: tpl.RequireActiveVersion,
|
|
deprecationMessage: tpl.Deprecated,
|
|
useClassicTemplateFlow: tpl.UseClassicParameterFlow,
|
|
disableModuleCache: tpl.DisableModuleCache,
|
|
corsBehavior: tpl.CorsBehavior,
|
|
autostopRequirementDaysOfWeekParsed: 0b0000001,
|
|
autostartRequirementDaysOfWeekParsed: 0b1000000,
|
|
autostopRequirementWeeks: tpl.AutostopRequirementWeeks,
|
|
groupACL: tpl.GroupACL,
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateMetaUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type expected struct {
|
|
// override is applied to baselineResolved to produce the expected
|
|
// templateMetaUpdate. Allows each case to express only its delta.
|
|
override func(*templateMetaUpdate)
|
|
base func(template *database.Template)
|
|
// validErrFields, if non-empty, asserts the resolver produced a
|
|
// validation error for each named field.
|
|
validErrFields []string
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
req codersdk.UpdateTemplateMeta
|
|
expected expected
|
|
}{
|
|
// Sanity check: an empty PATCH preserves every field.
|
|
{
|
|
name: "EmptyRequestPreservesEverything",
|
|
req: codersdk.UpdateTemplateMeta{},
|
|
expected: expected{override: func(*templateMetaUpdate) {}},
|
|
},
|
|
|
|
// One case per pointer field: each case sends only that field
|
|
// and asserts only that field changed in the resolved struct.
|
|
{
|
|
name: "Name",
|
|
req: codersdk.UpdateTemplateMeta{Name: ptr.Ref("renamed")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.name = "renamed"
|
|
}},
|
|
},
|
|
{
|
|
name: "NameEmptyStringFallsBackToCurrent",
|
|
req: codersdk.UpdateTemplateMeta{Name: ptr.Ref("")},
|
|
// Empty string is treated as "do not clear" because the UI
|
|
// disallows clearing the name. Resolver must keep the
|
|
// existing name.
|
|
// This is a unique case to just the `name` field.
|
|
expected: expected{override: func(*templateMetaUpdate) {}},
|
|
},
|
|
{
|
|
name: "DisplayName",
|
|
req: codersdk.UpdateTemplateMeta{DisplayName: ptr.Ref("Renamed")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.displayName = "Renamed"
|
|
}},
|
|
},
|
|
{
|
|
name: "Description",
|
|
req: codersdk.UpdateTemplateMeta{Description: ptr.Ref("New description")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.description = "New description"
|
|
}},
|
|
},
|
|
{
|
|
name: "Icon",
|
|
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("/new.svg")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.icon = "/new.svg"
|
|
}},
|
|
},
|
|
{
|
|
name: "DefaultTTLMillis",
|
|
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: ptr.Ref(int64(7200_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.defaultTTLMillis = 7200_000
|
|
}},
|
|
},
|
|
{
|
|
name: "DefaultTTLMillisZeroExplicit",
|
|
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: ptr.Ref(int64(0))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.defaultTTLMillis = 0
|
|
}},
|
|
},
|
|
{
|
|
name: "ActivityBumpMillis",
|
|
req: codersdk.UpdateTemplateMeta{ActivityBumpMillis: ptr.Ref(int64(900_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.activityBumpMillis = 900_000
|
|
}},
|
|
},
|
|
{
|
|
name: "AllowUserAutostart",
|
|
req: codersdk.UpdateTemplateMeta{AllowUserAutostart: ptr.Ref(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.allowUserAutostart = true
|
|
}},
|
|
},
|
|
{
|
|
name: "AllowUserAutostop",
|
|
req: codersdk.UpdateTemplateMeta{AllowUserAutostop: ptr.Ref(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.allowUserAutostop = true
|
|
}},
|
|
},
|
|
{
|
|
name: "AllowUserAutostop/true",
|
|
req: codersdk.UpdateTemplateMeta{AllowUserAutostop: ptr.Ref(false)},
|
|
expected: expected{
|
|
base: func(update *database.Template) {
|
|
update.AllowUserAutostop = true
|
|
},
|
|
override: func(r *templateMetaUpdate) {
|
|
r.allowUserAutostop = false
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "AllowUserCancelWorkspaceJobs",
|
|
req: codersdk.UpdateTemplateMeta{AllowUserCancelWorkspaceJobs: ptr.Ref(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.allowUserCancelWorkspaceJobs = true
|
|
}},
|
|
},
|
|
{
|
|
name: "FailureTTLMillis",
|
|
req: codersdk.UpdateTemplateMeta{FailureTTLMillis: ptr.Ref(int64(3_600_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.failureTTLMillis = 3_600_000
|
|
}},
|
|
},
|
|
{
|
|
name: "TimeTilDormantMillis",
|
|
req: codersdk.UpdateTemplateMeta{TimeTilDormantMillis: ptr.Ref(int64(7_200_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.timeTilDormantMillis = 7_200_000
|
|
}},
|
|
},
|
|
{
|
|
name: "TimeTilDormantAutoDeleteMillis",
|
|
req: codersdk.UpdateTemplateMeta{TimeTilDormantAutoDeleteMillis: ptr.Ref(int64(14_400_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.timeTilDormantAutoDeleteMillis = 14_400_000
|
|
}},
|
|
},
|
|
{
|
|
name: "RequireActiveVersion",
|
|
req: codersdk.UpdateTemplateMeta{RequireActiveVersion: ptr.Ref(false)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.requireActiveVersion = false
|
|
}},
|
|
},
|
|
{
|
|
name: "DeprecationMessage",
|
|
req: codersdk.UpdateTemplateMeta{DeprecationMessage: ptr.Ref("now deprecated")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.deprecationMessage = "now deprecated"
|
|
}},
|
|
},
|
|
{
|
|
name: "DeprecationMessageEmptyStringClears",
|
|
req: codersdk.UpdateTemplateMeta{DeprecationMessage: ptr.Ref("")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.deprecationMessage = ""
|
|
}},
|
|
},
|
|
{
|
|
name: "UseClassicParameterFlow",
|
|
req: codersdk.UpdateTemplateMeta{UseClassicParameterFlow: ptr.Ref(false)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.useClassicTemplateFlow = false
|
|
}},
|
|
},
|
|
{
|
|
name: "DisableModuleCache",
|
|
req: codersdk.UpdateTemplateMeta{DisableModuleCache: ptr.Ref(false)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.disableModuleCache = false
|
|
}},
|
|
},
|
|
|
|
// CORS behavior.
|
|
{
|
|
name: "CORSBehaviorChange",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
CORSBehavior: ptr.Ref(codersdk.CORSBehavior(database.CorsBehaviorSimple)),
|
|
},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.corsBehavior = database.CorsBehaviorSimple
|
|
}},
|
|
},
|
|
{
|
|
name: "CORSBehaviorEmptyStringPreserves",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
CORSBehavior: ptr.Ref(codersdk.CORSBehavior("")),
|
|
},
|
|
// Empty string is treated as "do not change" for backwards
|
|
// compatibility with older clients that always send the
|
|
// field.
|
|
expected: expected{override: func(*templateMetaUpdate) {}},
|
|
},
|
|
{
|
|
name: "CORSBehaviorInvalid",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
CORSBehavior: ptr.Ref(codersdk.CORSBehavior("not-a-real-value")),
|
|
},
|
|
expected: expected{
|
|
// Invalid value: keep current and surface a validation error.
|
|
override: func(*templateMetaUpdate) {},
|
|
validErrFields: []string{"cors_behavior"},
|
|
},
|
|
},
|
|
|
|
// Autostop / autostart requirement bitmaps.
|
|
{
|
|
name: "AutostopRequirementChange",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
|
DaysOfWeek: []string{"friday"},
|
|
Weeks: 4,
|
|
},
|
|
},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.autostopRequirementDaysOfWeekParsed = 0b0010000
|
|
r.autostopRequirementWeeks = 4
|
|
}},
|
|
},
|
|
{
|
|
name: "AutostopRequirementWeeksZeroNormalizesToOne",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
|
DaysOfWeek: []string{"monday"},
|
|
Weeks: 0,
|
|
},
|
|
},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.autostopRequirementDaysOfWeekParsed = 0b0000001
|
|
r.autostopRequirementWeeks = 1
|
|
}},
|
|
},
|
|
{
|
|
name: "AutostopRequirementInvalidDay",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
|
DaysOfWeek: []string{"funday"},
|
|
Weeks: 1,
|
|
},
|
|
},
|
|
expected: expected{
|
|
override: func(r *templateMetaUpdate) {
|
|
r.autostopRequirementDaysOfWeekParsed = 1
|
|
r.autostopRequirementWeeks = 2
|
|
},
|
|
validErrFields: []string{"autostop_requirement.days_of_week"},
|
|
},
|
|
},
|
|
{
|
|
name: "AutostartRequirementChange",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
|
DaysOfWeek: []string{"saturday"},
|
|
},
|
|
},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.autostartRequirementDaysOfWeekParsed = 0b0100000
|
|
}},
|
|
},
|
|
{
|
|
name: "AutostartRequirementInvalidDay",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
|
DaysOfWeek: []string{"funday"},
|
|
},
|
|
},
|
|
expected: expected{
|
|
override: func(r *templateMetaUpdate) {
|
|
r.autostartRequirementDaysOfWeekParsed = 64
|
|
},
|
|
validErrFields: []string{"autostart_requirement.days_of_week"},
|
|
},
|
|
},
|
|
|
|
// One-shot intent flags. nil and false should both result in
|
|
// the corresponding *Intent field being false; only true triggers it.
|
|
{
|
|
name: "DisableEveryoneGroupAccessFalseIsNoop",
|
|
req: codersdk.UpdateTemplateMeta{DisableEveryoneGroupAccess: ptr.Ref(false)},
|
|
expected: expected{override: func(*templateMetaUpdate) {
|
|
// disableEveryoneIntent stays false.
|
|
}},
|
|
},
|
|
{
|
|
name: "DisableEveryoneGroupAccessTrueWithMembership",
|
|
req: codersdk.UpdateTemplateMeta{DisableEveryoneGroupAccess: ptr.Ref(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.groupACL = database.TemplateACL{}
|
|
}},
|
|
},
|
|
{
|
|
name: "UpdateWorkspaceLastUsedAtFalseIsNoop",
|
|
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceLastUsedAt: ptr.Ref(false)},
|
|
expected: expected{override: func(*templateMetaUpdate) {}},
|
|
},
|
|
{
|
|
name: "UpdateWorkspaceLastUsedAtTrue",
|
|
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceLastUsedAt: ptr.Ref(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.updateWorkspaceLastUsedAtIntent = true
|
|
}},
|
|
},
|
|
{
|
|
name: "UpdateWorkspaceDormantAtFalseIsNoop",
|
|
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceDormantAt: ptr.Ref(false)},
|
|
expected: expected{override: func(*templateMetaUpdate) {}},
|
|
},
|
|
{
|
|
name: "UpdateWorkspaceDormantAtTrue",
|
|
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceDormantAt: ptr.Ref(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.updateWorkspaceDormantAtIntent = true
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tpl := baselineTemplate()
|
|
if tc.expected.base != nil {
|
|
tc.expected.base(&tpl)
|
|
}
|
|
schedOpts := baselineScheduleOpts()
|
|
got, validErrs := resolveTemplateMetaUpdate(tpl, schedOpts, tc.req)
|
|
|
|
want := baselineResolved()
|
|
tc.expected.override(&want)
|
|
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("resolved mismatch\ngot: %+v\nwant: %+v", got, want)
|
|
}
|
|
|
|
if len(validErrs) != len(tc.expected.validErrFields) {
|
|
t.Fatalf("got %d validation errors, want %d: %+v",
|
|
len(validErrs), len(tc.expected.validErrFields), validErrs)
|
|
}
|
|
for i, field := range tc.expected.validErrFields {
|
|
if validErrs[i].Field != field {
|
|
t.Errorf("validation error %d: field = %q, want %q",
|
|
i, validErrs[i].Field, field)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResolveTemplateMetaUpdate_NameClearedFallsBackToTemplateName covers the
|
|
// pre-existing rule that a template's name cannot be cleared from the UI.
|
|
// Even an explicit empty pointer must resolve to the existing template name.
|
|
func TestResolveTemplateMetaUpdate_NameClearedFallsBackToTemplateName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tpl := baselineTemplate()
|
|
schedOpts := baselineScheduleOpts()
|
|
|
|
got, _ := resolveTemplateMetaUpdate(tpl, schedOpts, codersdk.UpdateTemplateMeta{
|
|
Name: ptr.Ref(""),
|
|
})
|
|
if got.name != tpl.Name {
|
|
t.Fatalf("got name = %q, want %q (preserved)", got.name, tpl.Name)
|
|
}
|
|
}
|
|
|
|
// TestResolveTemplateMetaUpdate_NilRequestUsesScheduleOptsForRequirements
|
|
// verifies that an entirely empty request returns the schedule store's
|
|
// current autostop/autostart requirement values, rather than zeros.
|
|
func TestResolveTemplateMetaUpdate_NilRequestUsesScheduleOptsForRequirements(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tpl := baselineTemplate()
|
|
schedOpts := schedule.TemplateScheduleOptions{
|
|
AutostopRequirement: schedule.TemplateAutostopRequirement{
|
|
DaysOfWeek: 0b0001100, // Wed + Thu
|
|
Weeks: 3,
|
|
},
|
|
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
|
DaysOfWeek: 0b0010000, // Fri
|
|
},
|
|
}
|
|
|
|
got, validErrs := resolveTemplateMetaUpdate(tpl, schedOpts, codersdk.UpdateTemplateMeta{})
|
|
if len(validErrs) != 0 {
|
|
t.Fatalf("unexpected validation errors: %+v", validErrs)
|
|
}
|
|
if got.autostopRequirementDaysOfWeekParsed != schedOpts.AutostopRequirement.DaysOfWeek {
|
|
t.Errorf("autostop days = 0b%07b, want 0b%07b",
|
|
got.autostopRequirementDaysOfWeekParsed,
|
|
schedOpts.AutostopRequirement.DaysOfWeek)
|
|
}
|
|
if got.autostartRequirementDaysOfWeekParsed != schedOpts.AutostartRequirement.DaysOfWeek {
|
|
t.Errorf("autostart days = 0b%07b, want 0b%07b",
|
|
got.autostartRequirementDaysOfWeekParsed,
|
|
schedOpts.AutostartRequirement.DaysOfWeek)
|
|
}
|
|
if got.autostopRequirementWeeks != schedOpts.AutostopRequirement.Weeks {
|
|
t.Errorf("autostop weeks = %d, want %d",
|
|
got.autostopRequirementWeeks, schedOpts.AutostopRequirement.Weeks)
|
|
}
|
|
}
|