Files
coder/coderd/templates_meta_update_internal_test.go
Ethan c650aabbef chore: standardize on *_internal_test.go for white-box tests (#25601)
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.
2026-05-22 20:24:38 +10:00

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)
}
}