diff --git a/pkg/pusher/ocipusher_test.go b/pkg/pusher/ocipusher_test.go index 760da8404..24f52a7ad 100644 --- a/pkg/pusher/ocipusher_test.go +++ b/pkg/pusher/ocipusher_test.go @@ -1,3 +1,5 @@ +//go:build !windows + /* Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,7 +18,10 @@ limitations under the License. package pusher import ( + "io" + "os" "path/filepath" + "strings" "testing" "helm.sh/helm/v4/pkg/registry" @@ -94,3 +99,330 @@ func TestNewOCIPusher(t *testing.T) { t.Errorf("Expected NewOCIPusher to contain %p as RegistryClient, got %p", registryClient, op.opts.registryClient) } } + +func TestOCIPusher_Push_ErrorHandling(t *testing.T) { + tests := []struct { + name string + chartRef string + expectedError string + setupFunc func() string + }{ + { + name: "non-existent file", + chartRef: "/non/existent/file.tgz", + expectedError: "no such file", + }, + { + name: "directory instead of file", + expectedError: "cannot push directory, must provide chart archive (.tgz)", + setupFunc: func() string { + tempDir := t.TempDir() + return tempDir + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pusher, err := NewOCIPusher() + if err != nil { + t.Fatal(err) + } + + chartRef := tt.chartRef + if tt.setupFunc != nil { + chartRef = tt.setupFunc() + } + + err = pusher.Push(chartRef, "oci://localhost:5000/test") + if err == nil { + t.Fatal("Expected error but got none") + } + + if !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("Expected error containing %q, got %q", tt.expectedError, err.Error()) + } + }) + } +} + +func TestOCIPusher_newRegistryClient(t *testing.T) { + cd := "../../testdata" + join := filepath.Join + ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem") + + tests := []struct { + name string + opts []Option + expectError bool + errorContains string + }{ + { + name: "plain HTTP", + opts: []Option{WithPlainHTTP(true)}, + }, + { + name: "with TLS client config", + opts: []Option{ + WithTLSClientConfig(pub, priv, ca), + }, + }, + { + name: "with insecure skip TLS verify", + opts: []Option{ + WithInsecureSkipTLSVerify(true), + }, + }, + { + name: "with cert and key only", + opts: []Option{ + WithTLSClientConfig(pub, priv, ""), + }, + }, + { + name: "with CA file only", + opts: []Option{ + WithTLSClientConfig("", "", ca), + }, + }, + { + name: "default client without options", + opts: []Option{}, + }, + { + name: "invalid cert file", + opts: []Option{ + WithTLSClientConfig("/non/existent/cert.pem", priv, ca), + }, + expectError: true, + errorContains: "can't create TLS config", + }, + { + name: "invalid key file", + opts: []Option{ + WithTLSClientConfig(pub, "/non/existent/key.pem", ca), + }, + expectError: true, + errorContains: "can't create TLS config", + }, + { + name: "invalid CA file", + opts: []Option{ + WithTLSClientConfig("", "", "/non/existent/ca.crt"), + }, + expectError: true, + errorContains: "can't create TLS config", + }, + { + name: "combined TLS options", + opts: []Option{ + WithTLSClientConfig(pub, priv, ca), + WithInsecureSkipTLSVerify(true), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pusher, err := NewOCIPusher(tt.opts...) + if err != nil { + t.Fatal(err) + } + + op, ok := pusher.(*OCIPusher) + if !ok { + t.Fatal("Expected *OCIPusher") + } + + client, err := op.newRegistryClient() + if tt.expectError { + if err == nil { + t.Fatal("Expected error but got none") + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error containing %q, got %q", tt.errorContains, err.Error()) + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if client == nil { + t.Fatal("Expected non-nil registry client") + } + } + }) + } +} + +func TestOCIPusher_Push_ChartOperations(t *testing.T) { + // Path to test charts + chartPath := "../../pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz" + chartWithProvPath := "../../pkg/cmd/testdata/testcharts/signtest-0.1.0.tgz" + + tests := []struct { + name string + chartRef string + href string + options []Option + setupFunc func(t *testing.T) (string, func()) + expectError bool + errorContains string + }{ + { + name: "invalid chart file", + chartRef: "../../pkg/action/testdata/charts/corrupted-compressed-chart.tgz", + href: "oci://localhost:5000/test", + expectError: true, + errorContains: "does not appear to be a gzipped archive", + }, + { + name: "chart read error", + setupFunc: func(t *testing.T) (string, func()) { + t.Helper() + // Create a valid chart file that we'll make unreadable + tempDir := t.TempDir() + tempChart := filepath.Join(tempDir, "temp-chart.tgz") + + // Copy a valid chart + src, err := os.Open(chartPath) + if err != nil { + t.Fatal(err) + } + defer src.Close() + + dst, err := os.Create(tempChart) + if err != nil { + t.Fatal(err) + } + + if _, err := io.Copy(dst, src); err != nil { + t.Fatal(err) + } + dst.Close() + + // Make the file unreadable + if err := os.Chmod(tempChart, 0000); err != nil { + t.Fatal(err) + } + + return tempChart, func() { + os.Chmod(tempChart, 0644) // Restore permissions for cleanup + } + }, + href: "oci://localhost:5000/test", + expectError: true, + errorContains: "permission denied", + }, + { + name: "push with provenance file - loading phase", + chartRef: chartWithProvPath, + href: "oci://registry.example.com/charts", + setupFunc: func(t *testing.T) (string, func()) { + t.Helper() + // Copy chart and create a .prov file for it + tempDir := t.TempDir() + tempChart := filepath.Join(tempDir, "signtest-0.1.0.tgz") + tempProv := filepath.Join(tempDir, "signtest-0.1.0.tgz.prov") + + // Copy chart file + src, err := os.Open(chartWithProvPath) + if err != nil { + t.Fatal(err) + } + defer src.Close() + + dst, err := os.Create(tempChart) + if err != nil { + t.Fatal(err) + } + + if _, err := io.Copy(dst, src); err != nil { + t.Fatal(err) + } + dst.Close() + + // Create provenance file + if err := os.WriteFile(tempProv, []byte("test provenance data"), 0644); err != nil { + t.Fatal(err) + } + + return tempChart, func() {} + }, + expectError: true, // Will fail at the registry push step + errorContains: "", // Error depends on registry client behavior + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chartRef := tt.chartRef + var cleanup func() + + if tt.setupFunc != nil { + chartRef, cleanup = tt.setupFunc(t) + if cleanup != nil { + defer cleanup() + } + } + + // Skip test if chart file doesn't exist and we're not expecting an error + if _, err := os.Stat(chartRef); err != nil && !tt.expectError { + t.Skipf("Test chart %s not found, skipping test", chartRef) + } + + pusher, err := NewOCIPusher(tt.options...) + if err != nil { + t.Fatal(err) + } + + err = pusher.Push(chartRef, tt.href) + + if tt.expectError { + if err == nil { + t.Fatal("Expected error but got none") + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error containing %q, got %q", tt.errorContains, err.Error()) + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } + }) + } +} + +func TestOCIPusher_Push_MultipleOptions(t *testing.T) { + chartPath := "../../pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz" + + // Skip test if chart file doesn't exist + if _, err := os.Stat(chartPath); err != nil { + t.Skipf("Test chart %s not found, skipping test", chartPath) + } + + pusher, err := NewOCIPusher() + if err != nil { + t.Fatal(err) + } + + // Test that multiple options are applied correctly + err = pusher.Push(chartPath, "oci://localhost:5000/test", + WithPlainHTTP(true), + WithInsecureSkipTLSVerify(true), + ) + + // We expect an error since we're not actually pushing to a registry + if err == nil { + t.Fatal("Expected error when pushing without a valid registry") + } + + // Verify options were applied + op := pusher.(*OCIPusher) + if !op.opts.plainHTTP { + t.Error("Expected plainHTTP option to be applied") + } + if !op.opts.insecureSkipTLSverify { + t.Error("Expected insecureSkipTLSverify option to be applied") + } +}