Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions internal/helm/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ func removeChartDir(tb testing.TB, path string) {
}
}

// cleanupChartCache removes every backing directory still stored in cache
// when the test finishes. Register it before the call that fills the cache.
func cleanupChartCache(tb testing.TB, cache *ChartCache) {
tb.Helper()
tb.Cleanup(func() {
cache.mu.Lock()
paths := make([]string, 0, len(cache.entries))
for _, entry := range cache.entries {
paths = append(paths, entry.path)
}
cache.mu.Unlock()
for _, path := range paths {
if err := os.RemoveAll(path); err != nil {
tb.Logf("cleanup: remove chart cache %s: %v", path, err)
}
}
})
}

// TestPrepareChart_RequiresCache verifies PrepareChart rejects a nil cache.
func TestPrepareChart_RequiresCache(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -133,17 +152,16 @@ func TestNewChartCache_NonPositiveTTLUsesDefault(t *testing.T) {
// unknown keys, TTL expiry, and deleted backing directories.
func TestChartCache_GetMissExpiredAndMissingDir(t *testing.T) {
t.Parallel()
cache := NewChartCache(time.Millisecond)
cache := NewChartCache(time.Hour)

_, found := cache.get("missing")
assert.False(t, found)

dir := t.TempDir()
cache.set("expired", dir)
require.Eventually(t, func() bool {
_, ok := cache.get("expired")
return !ok
}, 50*time.Millisecond, time.Millisecond)
cache.backdate(t, "expired", 2*time.Hour)
_, found = cache.get("expired")
assert.False(t, found)
assert.Equal(t, 1, cache.entryCount(), "expired entries remain until cleanup")

gone := filepath.Join(t.TempDir(), "removed")
Expand Down Expand Up @@ -203,6 +221,14 @@ func TestCreateChartCopy_CopiesFiles(t *testing.T) {
assert.NotEqual(t, src, dst)
}

func TestCreateChartCopy_MissingSource(t *testing.T) {
t.Parallel()
_, err := createChartCopy(filepath.Join(t.TempDir(), "missing"))
require.Error(t, err)
assert.ErrorIs(t, err, os.ErrNotExist)
assert.ErrorContains(t, err, "failed to copy cached chart")
}

// TestPrepareChart_CanceledContextReturnsImmediately verifies PrepareChart
// honors an already-canceled context before doing chart work.
func TestPrepareChart_CanceledContextReturnsImmediately(t *testing.T) {
Expand Down
84 changes: 84 additions & 0 deletions internal/helm/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2026 The Deployah 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 helm

import (
"errors"
"io"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

kubefake "helm.sh/helm/v4/pkg/kube/fake"
)

func TestNewClient_RejectsInvalidStorageDriver(t *testing.T) {
t.Parallel()

c, err := NewClient(WithStorageDriver("sql"))
require.Error(t, err)
assert.Nil(t, c)
assert.ErrorContains(t, err, "invalid storage driver 'sql'")
}

func TestNewClient_TimeoutAndNamespaceReachHelmActions(t *testing.T) {
t.Parallel()

c, err := NewClient(
WithStorageDriver("memory"),
WithTimeout(90*time.Second),
WithNamespace("apps"),
)
require.NoError(t, err)
install := c.newInstallAction("shop-production", nil, nil, false)
upgrade := c.newUpgradeAction(nil, nil)
assert.Equal(t, 90*time.Second, install.Timeout)
assert.Equal(t, 90*time.Second, upgrade.Timeout)
assert.Equal(t, "apps", install.Namespace)
assert.Equal(t, "apps", upgrade.Namespace)

defaults, err := NewClient(WithStorageDriver("memory"), WithNamespace(""))
require.NoError(t, err)
assert.Equal(t, "default", defaults.Namespace())
assert.Equal(t, 5*time.Minute, defaults.newInstallAction("shop-production", nil, nil, false).Timeout)
}

func TestNamespace_EmptyAndNilUseDefault(t *testing.T) {
t.Parallel()

var c *Client
assert.Equal(t, "default", c.Namespace())
assert.Equal(t, "default", (&Client{}).Namespace())
assert.Equal(t, "apps", (&Client{namespace: "apps"}).Namespace())
}

func TestIsReachable(t *testing.T) {
t.Parallel()

c, cfg, _, _ := memoryHelmApp(t, "reach-app")
require.NoError(t, c.IsReachable())

cause := errors.New("api server unreachable")
failing := &kubefake.FailingKubeClient{ConnectionError: cause}
failing.Out = io.Discard
cfg.KubeClient = failing

err := c.IsReachable()
require.Error(t, err)
assert.ErrorIs(t, err, ErrClusterUnreachable)
assert.ErrorIs(t, err, cause)
}
43 changes: 43 additions & 0 deletions internal/helm/dest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ func TestRESTClientGetter_UnconfiguredDiscoveryFails(t *testing.T) {
require.ErrorIs(t, err, ErrDestinationNotConfigured)
_, err = getter.ToRESTMapper()
require.ErrorIs(t, err, ErrDestinationNotConfigured)

loader := getter.ToRawKubeConfigLoader()
_, err = loader.RawConfig()
require.ErrorIs(t, err, ErrDestinationNotConfigured)
ns, overridden, err := loader.Namespace()
require.NoError(t, err)
assert.Equal(t, "default", ns)
assert.False(t, overridden)
assert.NotNil(t, loader.ConfigAccess())
}

func TestRESTClientGetter_NoInClusterFallback(t *testing.T) {
Expand Down Expand Up @@ -211,11 +220,45 @@ func TestNewClient_UnconfiguredGetterIgnoresAmbientKubeconfig(t *testing.T) {

err = client.IsReachable()
require.Error(t, err)
assert.ErrorIs(t, err, ErrClusterUnreachable)
assert.ErrorIs(t, err, ErrDestinationNotConfigured)
})
}
}

func TestDefaultKubeCacheDir(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("KUBECACHEDIR", "")
assert.Equal(t, filepath.Join(home, ".kube", "cache"), defaultKubeCacheDir())

explicit := t.TempDir()
t.Setenv("KUBECACHEDIR", explicit)
assert.Equal(t, explicit, defaultKubeCacheDir())
}

func TestComputeDiscoverCacheDir(t *testing.T) {
t.Parallel()

tests := []struct {
name string
host string
want string
}{
{name: "https host and port", host: "https://prod.example:6443", want: "prod.example_6443"},
{name: "http address", host: "http://127.0.0.1:8080", want: "127.0.0.1_8080"},
{name: "path separator stays", host: "https://api.example/cluster", want: "api.example/cluster"},
{name: "space becomes underscore", host: "https://bad host", want: "bad_host"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := computeDiscoverCacheDir("cache", tt.host)
assert.Equal(t, filepath.Join("cache", tt.want), got)
})
}
}

func writeDestKubeconfig(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "kubeconfig")
Expand Down
79 changes: 79 additions & 0 deletions internal/helm/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,20 @@ func TestBuildLivenessProbe_RestartAfterOnlyDefaultsInterval(t *testing.T) {
assert.Equal(t, 10, p["periodSeconds"])
}

// TestBuildLivenessProbe_InvalidDurationUsesDefaults verifies a duration
// that slips past validation still produces the default probe instead of
// failing the render.
func TestBuildLivenessProbe_InvalidDurationUsesDefaults(t *testing.T) {
t.Parallel()

p, err := buildLivenessProbe(nil, "/livez", "nope", "also-nope")
require.NoError(t, err)
assert.Equal(t, spec.DefaultLivenessProbePeriod, p["periodSeconds"])
assert.Equal(t, spec.DefaultLivenessRestartAfterSec/spec.DefaultLivenessProbePeriod, p["failureThreshold"])
httpGet := mustNestedMap(t, p, "httpGet")
assert.Equal(t, "/livez", httpGet["path"])
}

// TestMapSpecToChartValues_EnvironmentFilterPrefixMatch verifies the
// component environments filter uses the same exact-then-prefix matching as
// spec.Resolve, so resolution and the generated chart agree on wildcard
Expand Down Expand Up @@ -509,6 +523,70 @@ func TestMapSpecToChartValues_CertManagerTLS(t *testing.T) {
assert.Equal(t, "letsencrypt-prod", annotations["cert-manager.io/cluster-issuer"])
}

// TestMapSpecToChartValues_ExposeWithoutTLS verifies an exposed service with
// no TLS mode renders ingress TLS off, so the chart does not request a cert.
func TestMapSpecToChartValues_ExposeWithoutTLS(t *testing.T) {
t.Parallel()

m := &spec.Spec{
APIVersion: spec.CurrentManifestVersion,
Project: "shop",
Components: map[string]spec.Component{
"api": {
Role: spec.ComponentRoleService,
Image: "shop-api:latest",
Port: 8080,
Expose: &spec.Expose{},
},
},
}
require.NoError(t, spec.FillSpecWithDefaults(m, spec.CurrentManifestVersion))
resolved := &spec.ResolvedSpec{
Spec: m,
Env: spec.NormalizeEnv("production"),
Components: map[string]spec.ResolvedComponent{
"api": {FQDN: "api.example.com"},
},
}

vals, err := MapSpecToChartValues(m, "production", resolved)
require.NoError(t, err)
ingress := mustNestedMap(t, mustNestedMap(t, vals, "api"), "ingress")
assert.Equal(t, true, ingress["enabled"])
assert.Equal(t, "api.example.com", ingress["hostname"])
assert.Equal(t, false, ingress["tls"])
assert.NotContains(t, ingress, "secrets")
assert.NotContains(t, ingress, "existingSecret")
}

func TestMapSpecToChartValues_ImageDigestAndArgs(t *testing.T) {
t.Parallel()

const digest = "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
m := &spec.Spec{
APIVersion: spec.CurrentManifestVersion,
Project: "shop",
Components: map[string]spec.Component{
"api": {
Role: spec.ComponentRoleService,
Image: "ghcr.io/acme/shop@" + digest,
Port: 8080,
Args: []string{"--verbose"},
},
},
}
require.NoError(t, spec.FillSpecWithDefaults(m, spec.CurrentManifestVersion))

vals, err := MapSpecToChartValues(m, "production", nil)
require.NoError(t, err)
api := mustNestedMap(t, vals, "api")
image := mustNestedMap(t, api, "image")
assert.Equal(t, "ghcr.io/acme/shop", image["repository"])
assert.Equal(t, digest, image["digest"])
assert.NotContains(t, image, "tag")
assert.Equal(t, []string{"--verbose"}, api["args"])
}

// TestMapSpecToChartValues_Autoscaling maps enabled HPA settings into values.
func TestMapSpecToChartValues_Autoscaling(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -1091,6 +1169,7 @@ func TestMapSpecToChartValues_WorkerStateless(t *testing.T) {
assert.Equal(t, 60, worker["terminationGracePeriodSeconds"])
assert.Nil(t, worker["startupProbe"])
assert.Nil(t, worker["readinessProbe"])
assert.Equal(t, []string{"sleep", "infinity"}, worker["command"])
resolved := mustNestedMap(t, mustNestedMap(t, mustNestedMap(t, values, "deployah"), "resolved"), "components")
assert.Equal(t, "worker", mustNestedMap(t, resolved, "worker")["role"])
}
Expand Down
46 changes: 46 additions & 0 deletions internal/helm/helm_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
Expand Down Expand Up @@ -185,6 +186,21 @@ func TestNewestRelease_SortsByVersion(t *testing.T) {
},
want: 5,
},
{
name: "nil entries sort after real revisions",
in: []*v1.Release{
nil,
{Version: 2},
nil,
{Version: 4},
},
want: 4,
},
{
name: "only nil entries",
in: []*v1.Release{nil, nil},
want: 0,
},
}

for _, tt := range tests {
Expand All @@ -201,6 +217,36 @@ func TestNewestRelease_SortsByVersion(t *testing.T) {
}
}

func TestReleaserToV1(t *testing.T) {
t.Parallel()

pointer := &v1.Release{Name: "shop", Version: 2}
got, err := releaserToV1(pointer)
require.NoError(t, err)
assert.Same(t, pointer, got)

value := v1.Release{Name: "shop", Version: 3}
got, err = releaserToV1(value)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, 3, got.Version)

_, err = releaserToV1(struct{}{})
require.Error(t, err)
assert.ErrorContains(t, err, "unsupported helm release type")

list, err := releaserListToV1([]release.Releaser{nil, value, pointer})
require.NoError(t, err)
require.Len(t, list, 3)
assert.Nil(t, list[0])
assert.Equal(t, 3, list[1].Version)
assert.Same(t, pointer, list[2])

_, err = releaserListToV1([]release.Releaser{struct{}{}})
require.Error(t, err)
assert.ErrorContains(t, err, "unsupported helm release type")
}

// TestInstallApp_PendingReleaseRejects pins the typed pending pre-check: a
// release whose newest revision is pending must fail with
// [ErrReleasePending] before Helm upgrade runs.
Expand Down
Loading
Loading