@@ -16,20 +16,23 @@ package validator
1616
1717import (
1818 "context"
19+ stderrors "errors"
1920 "fmt"
2021 "io/fs"
2122 "path/filepath"
2223 "testing"
2324
2425 "gopkg.in/yaml.v3"
2526
27+ "github.com/NVIDIA/aicr/pkg/errors"
2628 "github.com/NVIDIA/aicr/pkg/recipe"
2729 "github.com/NVIDIA/aicr/pkg/snapshotter"
2830 "github.com/NVIDIA/aicr/pkg/validator/catalog"
2931 "github.com/NVIDIA/aicr/pkg/validator/ctrf"
3032 v1 "github.com/NVIDIA/aicr/pkg/validator/v1"
3133 "github.com/NVIDIA/aicr/recipes"
3234 corev1 "k8s.io/api/core/v1"
35+ "k8s.io/client-go/kubernetes"
3336)
3437
3538func TestNewDefaults (t * testing.T ) {
@@ -47,6 +50,9 @@ func TestNewDefaults(t *testing.T) {
4750 if v .NoCluster {
4851 t .Error ("NoCluster should default to false" )
4952 }
53+ if v .Kubeconfig != "" {
54+ t .Errorf ("Kubeconfig = %q, want empty" , v .Kubeconfig )
55+ }
5056 if len (v .Tolerations ) != 1 || v .Tolerations [0 ].Operator != corev1 .TolerationOpExists {
5157 t .Errorf ("Tolerations should default to tolerate-all, got %v" , v .Tolerations )
5258 }
@@ -59,6 +65,7 @@ func TestNewWithOptions(t *testing.T) {
5965 v := New (
6066 WithVersion ("1.0.0" ),
6167 WithCommit ("abc1234" ),
68+ WithKubeconfig ("/path/to/kubeconfig" ),
6269 WithNamespace ("custom-ns" ),
6370 WithRunID ("test-run" ),
6471 WithCleanup (false ),
@@ -73,6 +80,9 @@ func TestNewWithOptions(t *testing.T) {
7380 if v .Commit != "abc1234" {
7481 t .Errorf ("Commit = %q, want %q" , v .Commit , "abc1234" )
7582 }
83+ if v .Kubeconfig != "/path/to/kubeconfig" {
84+ t .Errorf ("Kubeconfig = %q, want %q" , v .Kubeconfig , "/path/to/kubeconfig" )
85+ }
7686 if v .Namespace != "custom-ns" {
7787 t .Errorf ("Namespace = %q, want %q" , v .Namespace , "custom-ns" )
7888 }
@@ -90,6 +100,84 @@ func TestNewWithOptions(t *testing.T) {
90100 }
91101}
92102
103+ // TestPrepareClusterPropagatesCustomKubeconfig verifies the run-scoped path
104+ // reaches cluster client creation without reading a kubeconfig file or
105+ // contacting Kubernetes. The injected factory fails before any cluster API
106+ // operation, keeping this regression test hermetic and fail-safe.
107+ func TestPrepareClusterPropagatesCustomKubeconfig (t * testing.T ) {
108+ t .Parallel ()
109+
110+ const wantKubeconfig = "/path/to/target-kubeconfig"
111+ wantErr := stderrors .New ("stop before cluster access" )
112+ v := New (WithKubeconfig (" " + wantKubeconfig + " " ))
113+
114+ var gotKubeconfig string
115+ v .kubeClientFactory = func (kubeconfig string ) (kubernetes.Interface , error ) {
116+ gotKubeconfig = kubeconfig
117+ return nil , wantErr
118+ }
119+
120+ _ , err := v .prepareCluster (t .Context (), nil , nil )
121+ if ! stderrors .Is (err , wantErr ) {
122+ t .Fatalf ("prepareCluster() error = %v, want wrapped injected error" , err )
123+ }
124+ if gotKubeconfig != wantKubeconfig {
125+ t .Errorf ("kubeconfig = %q, want %q" , gotKubeconfig , wantKubeconfig )
126+ }
127+ }
128+
129+ // TestPrepareClusterEmptyKubeconfigUsesDefaultClient verifies that empty input
130+ // is routed through default discovery without consulting the explicit-path
131+ // client factory. The environment is cleared so default discovery fails before
132+ // any cluster access, keeping the test hermetic.
133+ func TestPrepareClusterEmptyKubeconfigUsesDefaultClient (t * testing.T ) {
134+ t .Setenv ("KUBECONFIG" , "" )
135+ t .Setenv ("HOME" , t .TempDir ())
136+ t .Setenv ("USERPROFILE" , t .TempDir ())
137+ t .Setenv ("KUBERNETES_SERVICE_HOST" , "" )
138+ t .Setenv ("KUBERNETES_SERVICE_PORT" , "" )
139+
140+ wantFactoryErr := stderrors .New ("explicit-path factory called" )
141+ v := New (WithKubeconfig (" \t " ))
142+ factoryCalled := false
143+ v .kubeClientFactory = func (string ) (kubernetes.Interface , error ) {
144+ factoryCalled = true
145+ return nil , wantFactoryErr
146+ }
147+
148+ _ , err := v .prepareCluster (t .Context (), nil , nil )
149+ if err == nil {
150+ t .Fatal ("prepareCluster() error = nil, want default discovery error" )
151+ }
152+ if factoryCalled {
153+ t .Error ("prepareCluster() called explicit-path factory for empty kubeconfig" )
154+ }
155+ if stderrors .Is (err , wantFactoryErr ) {
156+ t .Errorf ("prepareCluster() error = %v, want default discovery error" , err )
157+ }
158+ }
159+
160+ // TestPrepareClusterRejectsMissingKubeconfig verifies that a typo in a
161+ // caller-supplied path is classified as invalid input before Kubernetes client
162+ // construction can relabel the filesystem error as an internal failure.
163+ func TestPrepareClusterRejectsMissingKubeconfig (t * testing.T ) {
164+ t .Parallel ()
165+
166+ kubeconfig := filepath .Join (t .TempDir (), "missing-kubeconfig" )
167+ v := New (WithKubeconfig (kubeconfig ))
168+
169+ _ , err := v .prepareCluster (t .Context (), nil , nil )
170+ if err == nil {
171+ t .Fatal ("prepareCluster() error = nil, want invalid request" )
172+ }
173+ if ! stderrors .Is (err , errors .New (errors .ErrCodeInvalidRequest , "" )) {
174+ t .Errorf ("prepareCluster() error = %v, want ErrCodeInvalidRequest" , err )
175+ }
176+ if ! stderrors .Is (err , fs .ErrNotExist ) {
177+ t .Errorf ("prepareCluster() error = %v, want wrapped fs.ErrNotExist" , err )
178+ }
179+ }
180+
93181func loadEmbeddedCatalog (t * testing.T ) * catalog.ValidatorCatalog {
94182 t .Helper ()
95183 cat , err := catalog .LoadWithDataProvider (context .Background (), nil , "" , "" )
0 commit comments