@ -27,10 +27,14 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/helmpath"
@ -52,18 +56,10 @@ func (t *TestHTTPGetter) Get(_ string, _ ...getter.Option) (*bytes.Buffer, error
var fakePluginB64 = "H4sIAAAAAAAAA+3SQUvDMBgG4Jz7K0LwapdvSxrwJig6mCKC5xHabBaXdDSt4L+3cQ56mV42ZPg+lw+SF5LwZmXf3OV206/rMGEnIgdG6zTJaDmee4y01FOlZpqGHJGZSsb1qS401sfOtpyz0FTup9xv+2dqNep/N/IP6zdHPSMVXCh1sH8yhtGMDBUFFTL1r4iIcXnUWxzwz/sP1rsrLkbfQGTvro11E4ZlmcucRNZHu04py1OO73OVi2Vbb7td9vp7nXevtvsKRpGVjfc2VMP2xf3t4mH5tHi5mz8ub+bPk9JXIvvr5wMAAAAAAAAAAAAAAAAAAAAAnLVPqwHcXQAoAAA="
func TestStripName ( t * testing . T ) {
if stripPluginName ( "fake-plugin-0.0.1.tar.gz" ) != "fake-plugin" {
t . Error ( "name does not match expected value" )
}
if stripPluginName ( "fake-plugin-0.0.1.tgz" ) != "fake-plugin" {
t . Error ( "name does not match expected value" )
}
if stripPluginName ( "fake-plugin.tgz" ) != "fake-plugin" {
t . Error ( "name does not match expected value" )
}
if stripPluginName ( "fake-plugin.tar.gz" ) != "fake-plugin" {
t . Error ( "name does not match expected value" )
}
assert . Equal ( t , "fake-plugin" , stripPluginName ( "fake-plugin-0.0.1.tar.gz" ) , "name does not match expected value" )
assert . Equal ( t , "fake-plugin" , stripPluginName ( "fake-plugin-0.0.1.tgz" ) , "name does not match expected value" )
assert . Equal ( t , "fake-plugin" , stripPluginName ( "fake-plugin.tgz" ) , "name does not match expected value" )
assert . Equal ( t , "fake-plugin" , stripPluginName ( "fake-plugin.tar.gz" ) , "name does not match expected value" )
}
func mockArchiveServer ( ) * httptest . Server {
@ -85,45 +81,29 @@ func TestHTTPInstaller(t *testing.T) {
defer srv . Close ( )
source := srv . URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if err := os . MkdirAll ( helmpath . DataPath ( "plugins" ) , 0 o755 ) ; err != nil {
t . Fatalf ( "Could not create %s: %s" , helmpath . DataPath ( "plugins" ) , err )
}
require . NoErrorf ( t , os . MkdirAll ( helmpath . DataPath ( "plugins" ) , 0 o755 ) , "Could not create %s" , helmpath . DataPath ( "plugins" ) )
i , err := NewForSource ( source , "0.0.1" )
if err != nil {
t . Fatalf ( "unexpected error: %s" , err )
}
require . NoError ( t , err )
// ensure a HTTPInstaller was returned
httpInstaller , ok := i . ( * HTTPInstaller )
if ! ok {
t . Fatal ( "expected a HTTPInstaller" )
}
require . True ( t , ok , "expected a HTTPInstaller" )
// inject fake http client responding with minimal plugin tarball
mockTgz , err := base64 . StdEncoding . DecodeString ( fakePluginB64 )
if err != nil {
t . Fatalf ( "Could not decode fake tgz plugin: %s" , err )
}
require . NoError ( t , err , "Could not decode fake tgz plugin" )
httpInstaller . getter = & TestHTTPGetter {
MockResponse : bytes . NewBuffer ( mockTgz ) ,
}
// install the plugin
if err := Install ( i ) ; err != nil {
t . Fatal ( err )
}
if i . Path ( ) != helmpath . DataPath ( "plugins" , "fake-plugin" ) {
t . Fatalf ( "expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q" , i . Path ( ) )
}
require . NoError ( t , Install ( i ) )
require . Equal ( t , helmpath . DataPath ( "plugins" , "fake-plugin" ) , i . Path ( ) , "expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q" , i . Path ( ) )
// Install again to test plugin exists error
if err := Install ( i ) ; err == nil {
t . Fatal ( "expected error for plugin exists, got none" )
} else if err . Error ( ) != "plugin already exists" {
t . Fatalf ( "expected error for plugin exists, got (%v)" , err )
}
require . EqualErrorf ( t , Install ( i ) , "plugin already exists" , "expected error for plugin exists" )
}
func TestHTTPInstallerNonExistentVersion ( t * testing . T ) {
@ -132,20 +112,14 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
defer srv . Close ( )
source := srv . URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if err := os . MkdirAll ( helmpath . DataPath ( "plugins" ) , 0 o755 ) ; err != nil {
t . Fatalf ( "Could not create %s: %s" , helmpath . DataPath ( "plugins" ) , err )
}
require . NoErrorf ( t , os . MkdirAll ( helmpath . DataPath ( "plugins" ) , 0 o755 ) , "Could not create %s" , helmpath . DataPath ( "plugins" ) )
i , err := NewForSource ( source , "0.0.2" )
if err != nil {
t . Fatalf ( "unexpected error: %s" , err )
}
require . NoError ( t , err )
// ensure a HTTPInstaller was returned
httpInstaller , ok := i . ( * HTTPInstaller )
if ! ok {
t . Fatal ( "expected a HTTPInstaller" )
}
require . True ( t , ok , "expected a HTTPInstaller" )
// inject fake http client responding with error
httpInstaller . getter = & TestHTTPGetter {
@ -153,9 +127,7 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
}
// attempt to install the plugin
if err := Install ( i ) ; err == nil {
t . Fatal ( "expected error from http client" )
}
require . Error ( t , Install ( i ) , "expected error from http client" )
}
func TestHTTPInstallerUpdate ( t * testing . T ) {
@ -164,43 +136,29 @@ func TestHTTPInstallerUpdate(t *testing.T) {
source := srv . URL + "/plugins/fake-plugin-0.0.1.tar.gz"
ensure . HelmHome ( t )
if err := os . MkdirAll ( helmpath . DataPath ( "plugins" ) , 0 o755 ) ; err != nil {
t . Fatalf ( "Could not create %s: %s" , helmpath . DataPath ( "plugins" ) , err )
}
require . NoErrorf ( t , os . MkdirAll ( helmpath . DataPath ( "plugins" ) , 0 o755 ) , "Could not create %s" , helmpath . DataPath ( "plugins" ) )
i , err := NewForSource ( source , "0.0.1" )
if err != nil {
t . Fatalf ( "unexpected error: %s" , err )
}
require . NoError ( t , err )
// ensure a HTTPInstaller was returned
httpInstaller , ok := i . ( * HTTPInstaller )
if ! ok {
t . Fatal ( "expected a HTTPInstaller" )
}
require . True ( t , ok , "expected a HTTPInstaller" )
// inject fake http client responding with minimal plugin tarball
mockTgz , err := base64 . StdEncoding . DecodeString ( fakePluginB64 )
if err != nil {
t . Fatalf ( "Could not decode fake tgz plugin: %s" , err )
}
require . NoError ( t , err , "Could not decode fake tgz plugin" )
httpInstaller . getter = & TestHTTPGetter {
MockResponse : bytes . NewBuffer ( mockTgz ) ,
}
// install the plugin before updating
if err := Install ( i ) ; err != nil {
t . Fatal ( err )
}
if i . Path ( ) != helmpath . DataPath ( "plugins" , "fake-plugin" ) {
t . Fatalf ( "expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q" , i . Path ( ) )
}
require . NoError ( t , Install ( i ) )
require . Equal ( t , helmpath . DataPath ( "plugins" , "fake-plugin" ) , i . Path ( ) , "expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q" , i . Path ( ) )
// Update plugin, should fail because it is not implemented
if err := Update ( i ) ; err == nil {
t . Fatal ( "update method not implemented for http installer" )
}
require . Error ( t , Update ( i ) , "update method not implemented for http installer" )
}
func TestExtract ( t * testing . T ) {
@ -229,12 +187,9 @@ func TestExtract(t *testing.T) {
Mode : file . Mode ,
Size : int64 ( len ( file . Body ) ) ,
}
if err := tw . WriteHeader ( hdr ) ; err != nil {
t . Fatal ( err )
}
if _ , err := tw . Write ( [ ] byte ( file . Body ) ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , tw . WriteHeader ( hdr ) )
_ , err := tw . Write ( [ ] byte ( file . Body ) )
require . NoError ( t , err )
}
// Add pax global headers. This should be ignored.
@ -242,33 +197,24 @@ func TestExtract(t *testing.T) {
// Details are in the internal Go function for the tar packaged named
// allowedFormats. For a TypeXHeader it will return a message stating
// "cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers"
if err := tw . WriteHeader ( & tar . Header {
require . NoError ( t , tw . WriteHeader ( & tar . Header {
Name : "pax_global_header" ,
Typeflag : tar . TypeXGlobalHeader ,
} ) ; err != nil {
t . Fatal ( err )
}
} ) )
if err := tw . Close ( ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , tw . Close ( ) )
var buf bytes . Buffer
gz := gzip . NewWriter ( & buf )
if _ , err := gz . Write ( tarbuf . Bytes ( ) ) ; err != nil {
t . Fatal ( err )
}
_ , err := gz . Write ( tarbuf . Bytes ( ) )
require . NoError ( t , err )
gz . Close ( )
// END tarball creation
extractor , err := NewExtractor ( source )
if err != nil {
t . Fatal ( err )
}
require . NoError ( t , err )
if err = extractor . Extract ( & buf , tempDir ) ; err != nil {
t . Fatalf ( "Did not expect error but got error: %v" , err )
}
require . NoErrorf ( t , extractor . Extract ( & buf , tempDir ) , "Did not expect error" )
// Calculate expected permissions after umask is applied
expectedPluginYAMLPerm := os . FileMode ( 0 o600 &^ currentUmask )
@ -276,23 +222,19 @@ func TestExtract(t *testing.T) {
pluginYAMLFullPath := filepath . Join ( tempDir , "plugin.yaml" )
if info , err := os . Stat ( pluginYAMLFullPath ) ; err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
t . Fatalf ( "Expected %s to exist but doesn't" , pluginYAMLFullPath )
}
require . NotErrorIs ( t , err , fs . ErrNotExist , "Expected %s to exist but doesn't" , pluginYAMLFullPath )
t . Fatal ( err )
} else if info . Mode ( ) . Perm ( ) != expectedPluginYAMLPerm {
t. Fatalf ( "Expected %s to have %o mode but has %o (umask: %o)" ,
} else {
require. Equalf ( t , expectedPluginYAMLPerm , info . Mode ( ) . Perm ( ) , "Expected %s to have %o mode but has %o (umask: %o)" ,
pluginYAMLFullPath , expectedPluginYAMLPerm , info . Mode ( ) . Perm ( ) , currentUmask )
}
readmeFullPath := filepath . Join ( tempDir , "README.md" )
if info , err := os . Stat ( readmeFullPath ) ; err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
t . Fatalf ( "Expected %s to exist but doesn't" , readmeFullPath )
}
require . NotErrorIs ( t , err , fs . ErrNotExist , "Expected %s to exist but doesn't" , readmeFullPath )
t . Fatal ( err )
} else if info . Mode ( ) . Perm ( ) != expectedReadmePerm {
t. Fatalf ( "Expected %s to have %o mode but has %o (umask: %o)" ,
} else {
require. Equalf ( t , expectedReadmePerm , info . Mode ( ) . Perm ( ) , "Expected %s to have %o mode but has %o (umask: %o)" ,
readmeFullPath , expectedReadmePerm , info . Mode ( ) . Perm ( ) , currentUmask )
}
}
@ -313,16 +255,15 @@ func TestCleanJoin(t *testing.T) {
{ "foo\\bar.txt" , "/tmp/foo/bar.txt" , false } ,
{ "c:\\foo\\bar.txt" , "" , true } ,
} {
out , err := cleanJoin ( "/tmp" , fixture . path )
if err != nil {
if ! fixture . expectError {
t . Errorf ( "Test %d: Path was not cleaned: %s" , i , err )
t . Run ( strconv . Itoa ( i ) , func ( t * testing . T ) {
out , err := cleanJoin ( "/tmp" , fixture . path )
if fixture . expectError {
require . Error ( t , err , "Test %d: Path was not cleaned" , i )
} else {
require . NoError ( t , err )
assert . Equal ( t , fixture . expect , out , "Test %d: Expected %q but got %q" , i , fixture . expect , out )
}
continue
}
if fixture . expect != out {
t . Errorf ( "Test %d: Expected %q but got %q" , i , fixture . expect , out )
}
} )
}
}
@ -336,14 +277,11 @@ func TestMediaTypeToExtension(t *testing.T) {
"application/json" : false ,
} {
ext , ok := mediaTypeToExtension ( mt )
if ok != shouldPass {
t . Errorf ( "Media type %q failed test" , mt )
}
if shouldPass && ext == "" {
t . Error ( "Expected an extension but got empty string" )
}
if ! shouldPass && ext != "" {
t . Error ( "Expected extension to be empty for unrecognized type" )
assert . Equal ( t , shouldPass , ok , "Media type %q failed test" , mt )
if shouldPass {
assert . NotEmpty ( t , ext , "Expected an extension but got empty string for media type %q" , mt )
} else {
assert . Empty ( t , ext , "Expected extension to be empty for unrecognized media type %q" , mt )
}
}
}
@ -377,55 +315,41 @@ func TestExtractWithNestedDirectories(t *testing.T) {
Mode : file . Mode ,
Size : int64 ( len ( file . Body ) ) ,
}
if err := tw . WriteHeader ( hdr ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , tw . WriteHeader ( hdr ) )
if file . TypeFlag == tar . TypeReg {
if _ , err := tw . Write ( [ ] byte ( file . Body ) ) ; err != nil {
t . Fatal ( err )
}
_ , err := tw . Write ( [ ] byte ( file . Body ) )
require . NoError ( t , err )
}
}
if err := tw . Close ( ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , tw . Close ( ) )
var buf bytes . Buffer
gz := gzip . NewWriter ( & buf )
if _ , err := gz . Write ( tarbuf . Bytes ( ) ) ; err != nil {
t . Fatal ( err )
}
_ , err := gz . Write ( tarbuf . Bytes ( ) )
require . NoError ( t , err )
gz . Close ( )
extractor , err := NewExtractor ( source )
if err != nil {
t . Fatal ( err )
}
require . NoError ( t , err )
// First extraction
if err = extractor . Extract ( & buf , tempDir ) ; err != nil {
t . Fatalf ( "First extraction failed: %v" , err )
}
require . NoError ( t , extractor . Extract ( & buf , tempDir ) , "First extraction failed" )
// Verify nested structure was created
nestedFile := filepath . Join ( tempDir , "docs" , "examples" , "example1.yaml" )
if _ , err := os . Stat ( nestedFile ) ; err != nil {
t . Fatalf ( "Expected nested file %s to exist but got error: %v" , nestedFile , err )
}
_ , err = os . Stat ( nestedFile )
require . NoErrorf ( t , err , "Expected nested file %s to exist" , nestedFile )
// Reset buffer for second extraction
buf . Reset ( )
gz = gzip . NewWriter ( & buf )
if _ , err := gz . Write ( tarbuf . Bytes ( ) ) ; err != nil {
t . Fatal ( err )
}
_ , err = gz . Write ( tarbuf . Bytes ( ) )
require . NoError ( t , err )
gz . Close ( )
// Second extraction to same directory (should not fail)
if err = extractor . Extract ( & buf , tempDir ) ; err != nil {
t . Fatalf ( "Second extraction to existing directory failed: %v" , err )
}
require . NoErrorf ( t , extractor . Extract ( & buf , tempDir ) , "Second extraction to existing directory failed" )
}
func TestExtractWithExistingDirectory ( t * testing . T ) {
@ -434,15 +358,11 @@ func TestExtractWithExistingDirectory(t *testing.T) {
// Pre-create the cache directory structure
cacheDir := filepath . Join ( tempDir , "cache" )
if err := os . MkdirAll ( filepath . Join ( cacheDir , "existing" , "dir" ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , os . MkdirAll ( filepath . Join ( cacheDir , "existing" , "dir" ) , 0 o755 ) )
// Create a file in the existing directory
existingFile := filepath . Join ( cacheDir , "existing" , "file.txt" )
if err := os . WriteFile ( existingFile , [ ] byte ( "existing content" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , os . WriteFile ( existingFile , [ ] byte ( "existing content" ) , 0 o644 ) )
// Write a tarball
var tarbuf bytes . Buffer
@ -466,47 +386,35 @@ func TestExtractWithExistingDirectory(t *testing.T) {
Mode : file . Mode ,
Size : int64 ( len ( file . Body ) ) ,
}
if err := tw . WriteHeader ( hdr ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , tw . WriteHeader ( hdr ) )
if file . TypeFlag == tar . TypeReg {
if _ , err := tw . Write ( [ ] byte ( file . Body ) ) ; err != nil {
t . Fatal ( err )
}
_ , err := tw . Write ( [ ] byte ( file . Body ) )
require . NoError ( t , err )
}
}
if err := tw . Close ( ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , tw . Close ( ) )
var buf bytes . Buffer
gz := gzip . NewWriter ( & buf )
if _ , err := gz . Write ( tarbuf . Bytes ( ) ) ; err != nil {
t . Fatal ( err )
}
_ , err := gz . Write ( tarbuf . Bytes ( ) )
require . NoError ( t , err )
gz . Close ( )
extractor , err := NewExtractor ( source )
if err != nil {
t . Fatal ( err )
}
require . NoError ( t , err )
// Extract to directory with existing content
if err = extractor . Extract ( & buf , cacheDir ) ; err != nil {
t . Fatalf ( "Extraction to directory with existing content failed: %v" , err )
}
require . NoErrorf ( t , extractor . Extract ( & buf , cacheDir ) , "Extraction to directory with existing content failed" )
// Verify new file was created
newFile := filepath . Join ( cacheDir , "existing" , "dir" , "newfile.txt" )
if _ , err := os . Stat ( newFile ) ; err != nil {
t . Fatalf ( "Expected new file %s to exist but got error: %v" , newFile , err )
}
_ , err = os . Stat ( newFile )
require . NoErrorf ( t , err , "Expected new file %s to exist but got error" , newFile )
// Verify existing file is still there
if _ , err := os . Stat ( existingFile ) ; err != nil {
t . Fatalf ( "Expected existing file %s to still exist but got error: %v" , existingFile , err )
}
_ , err = os . Stat ( existingFile )
require . NoErrorf ( t , err , "Expected existing file %s to still exist" , existingFile )
}
func TestExtractPluginInSubdirectory ( t * testing . T ) {
@ -536,25 +444,19 @@ func TestExtractPluginInSubdirectory(t *testing.T) {
Mode : file . Mode ,
Size : int64 ( len ( file . Body ) ) ,
}
if err := tw . WriteHeader ( hdr ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , tw . WriteHeader ( hdr ) )
if file . TypeFlag == tar . TypeReg {
if _ , err := tw . Write ( [ ] byte ( file . Body ) ) ; err != nil {
t . Fatal ( err )
}
_ , err := tw . Write ( [ ] byte ( file . Body ) )
require . NoError ( t , err )
}
}
if err := tw . Close ( ) ; err != nil {
t . Fatal ( err )
}
require . NoError ( t , tw . Close ( ) )
var buf bytes . Buffer
gz := gzip . NewWriter ( & buf )
if _ , err := gz . Write ( tarbuf . Bytes ( ) ) ; err != nil {
t . Fatal ( err )
}
_ , err := gz . Write ( tarbuf . Bytes ( ) )
require . NoError ( t , err )
gz . Close ( )
// Test the installer
@ -573,24 +475,16 @@ func TestExtractPluginInSubdirectory(t *testing.T) {
// Ensure the destination directory doesn't exist
// (In a real scenario, this is handled by installer.Install() wrapper)
destPath := installer . Path ( )
if err := os . RemoveAll ( destPath ) ; err != nil {
t . Fatalf ( "Failed to clean destination path: %v" , err )
}
require . NoErrorf ( t , os . RemoveAll ( destPath ) , "Failed to clean destination path" )
// Install should handle the subdirectory correctly
if err := installer . Install ( ) ; err != nil {
t . Fatalf ( "Failed to install plugin with subdirectory: %v" , err )
}
require . NoErrorf ( t , installer . Install ( ) , "Failed to install plugin with subdirectory" )
// The plugin should be installed from the subdirectory
// Check that detectPluginRoot found the correct location
pluginRoot , err := detectPluginRoot ( tempDir )
if err != nil {
t . Fatalf ( "Failed to detect plugin root: %v" , err )
}
require . NoError ( t , err , "Failed to detect plugin root" )
expectedRoot := filepath . Join ( tempDir , "my-plugin" )
if pluginRoot != expectedRoot {
t . Errorf ( "Expected plugin root to be %s but got %s" , expectedRoot , pluginRoot )
}
assert . Equal ( t , expectedRoot , pluginRoot , "Expected plugin root to be %s but got %s" , expectedRoot , pluginRoot )
}