Files
cozystack/pkg/cmd/server/start_test.go
T
Myasnikov Daniil 30d84e99cb refactor(api): align HelmRelease conversion with operator, add validator tests
Production change:
- convertApplicationToHelmRelease now sets Install/Upgrade.Timeout
  unconditionally (matching cozystack-operator's buildHelmReleaseSpec).
  The previous "stay nil if zero" branch was a leaky test shim — it kept
  fixtures with empty ReleaseConfig{} green at the cost of api/operator
  drift.

Test changes:
- newRESTForTimeout seeds the new HelmRelease* globals with
  production-shaped defaults so the unset-annotation case exercises
  "global default applies" rather than "field stays nil".
- TestConvertApplicationToHelmRelease_PerAppTimeoutOverridesGlobal is
  table-driven and now covers asymmetric globals (install != upgrade)
  with and without per-app override — guards against a regression that
  only assigns one side under override.
- New start_test.go: table-driven validator coverage for each
  malformed/non-positive duration flag, the negative MaxHistory branch,
  the MaxHistory=0 unlimited branch, asymmetric timeout acceptance, and
  a smoke test that NewCozyServerOptions defaults validate.

Refactor:
- Extracted parseAndValidateHelmReleaseFlags from Complete() so the
  flag-validation portion is unit-testable without kubernetes I/O.

Documentation:
- Reworded --helmrelease-install/upgrade-timeout help so admins reading
  --help see the dual-purpose annotation explicitly (one annotation
  overrides both timeouts together).
- Dropped PR-#2509 references and "preserving the pre-#2509 behaviour
  for tests" comments — kept the technical rationale (Strategy.Name
  vs Remediation.Retries:-1 + 5m Interval coupling) on its own merits.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-06-22 09:48:26 +05:00

187 lines
5.8 KiB
Go

/*
Copyright 2026 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"strings"
"testing"
"time"
)
// validHelmReleaseFlags returns a CozyServerOptions populated with the same
// defaults NewCozyServerOptions sets, so each test case mutates a single
// flag and asserts the resulting validation behaviour in isolation.
func validHelmReleaseFlags() *CozyServerOptions {
return &CozyServerOptions{
HelmReleaseInterval: "5m",
HelmReleaseRetryInterval: "30s",
HelmReleaseInstallTimeout: "10m",
HelmReleaseUpgradeTimeout: "10m",
HelmReleaseMaxHistory: 5,
}
}
// Pins that each malformed/non-positive duration and a negative MaxHistory
// is rejected at startup. Without this, a regression that swaps `<` for
// `<=` on the MaxHistory branch — breaking the documented "0 means
// unlimited" contract — or that drops a `return err` line on any of the
// four duration branches would ship green.
func TestCozyServerOptions_ParseAndValidateHelmReleaseFlags_Rejects(t *testing.T) {
cases := []struct {
name string
mutate func(o *CozyServerOptions)
wantSubs string
}{
{
name: "interval zero",
mutate: func(o *CozyServerOptions) { o.HelmReleaseInterval = "0s" },
wantSubs: "--helmrelease-interval",
},
{
name: "interval negative",
mutate: func(o *CozyServerOptions) { o.HelmReleaseInterval = "-5m" },
wantSubs: "--helmrelease-interval",
},
{
name: "interval malformed",
mutate: func(o *CozyServerOptions) { o.HelmReleaseInterval = "5x" },
wantSubs: "--helmrelease-interval",
},
{
name: "retry-interval empty",
mutate: func(o *CozyServerOptions) { o.HelmReleaseRetryInterval = "" },
wantSubs: "--helmrelease-retry-interval",
},
{
name: "install-timeout zero",
mutate: func(o *CozyServerOptions) { o.HelmReleaseInstallTimeout = "0s" },
wantSubs: "--helmrelease-install-timeout",
},
{
name: "upgrade-timeout malformed",
mutate: func(o *CozyServerOptions) { o.HelmReleaseUpgradeTimeout = "tenminutes" },
wantSubs: "--helmrelease-upgrade-timeout",
},
{
name: "max-history negative",
mutate: func(o *CozyServerOptions) { o.HelmReleaseMaxHistory = -1 },
wantSubs: "--helmrelease-max-history must be >= 0",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
o := validHelmReleaseFlags()
tc.mutate(o)
_, err := o.parseAndValidateHelmReleaseFlags()
if err == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(err.Error(), tc.wantSubs) {
t.Errorf("error %q does not contain %q", err.Error(), tc.wantSubs)
}
})
}
}
// Pins the happy path: valid flag strings parse to the expected
// time.Duration values and MaxHistory=0 is accepted (the documented
// "unlimited per Helm semantics" branch).
func TestCozyServerOptions_ParseAndValidateHelmReleaseFlags_Accepts(t *testing.T) {
cases := []struct {
name string
o *CozyServerOptions
wantInterval time.Duration
wantRetry time.Duration
wantInstall time.Duration
wantUpgrade time.Duration
wantMaxHistory int
}{
{
name: "production defaults",
o: validHelmReleaseFlags(),
wantInterval: 5 * time.Minute,
wantRetry: 30 * time.Second,
wantInstall: 10 * time.Minute,
wantUpgrade: 10 * time.Minute,
wantMaxHistory: 5,
},
{
name: "max-history zero accepted as unlimited",
o: func() *CozyServerOptions {
o := validHelmReleaseFlags()
o.HelmReleaseMaxHistory = 0
return o
}(),
wantInterval: 5 * time.Minute,
wantRetry: 30 * time.Second,
wantInstall: 10 * time.Minute,
wantUpgrade: 10 * time.Minute,
wantMaxHistory: 0,
},
{
name: "asymmetric install/upgrade timeouts both honored",
o: func() *CozyServerOptions {
o := validHelmReleaseFlags()
o.HelmReleaseInstallTimeout = "8m"
o.HelmReleaseUpgradeTimeout = "12m"
return o
}(),
wantInterval: 5 * time.Minute,
wantRetry: 30 * time.Second,
wantInstall: 8 * time.Minute,
wantUpgrade: 12 * time.Minute,
wantMaxHistory: 5,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v, err := tc.o.parseAndValidateHelmReleaseFlags()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if v.interval != tc.wantInterval {
t.Errorf("interval = %v, want %v", v.interval, tc.wantInterval)
}
if v.retryInterval != tc.wantRetry {
t.Errorf("retryInterval = %v, want %v", v.retryInterval, tc.wantRetry)
}
if v.installTimeout != tc.wantInstall {
t.Errorf("installTimeout = %v, want %v", v.installTimeout, tc.wantInstall)
}
if v.upgradeTimeout != tc.wantUpgrade {
t.Errorf("upgradeTimeout = %v, want %v", v.upgradeTimeout, tc.wantUpgrade)
}
if v.maxHistory != tc.wantMaxHistory {
t.Errorf("maxHistory = %d, want %d", v.maxHistory, tc.wantMaxHistory)
}
})
}
}
// Pins that NewCozyServerOptions seeds defaults that pass validation, so
// `cozystack-api ...` with no HelmRelease flags overridden does not fail
// at startup. Guards against a future change to defaults that accidentally
// violates the validator.
func TestNewCozyServerOptions_DefaultsValidate(t *testing.T) {
o := NewCozyServerOptions(nil, nil)
if _, err := o.parseAndValidateHelmReleaseFlags(); err != nil {
t.Fatalf("default options must validate, got: %v", err)
}
}