get.porter.sh/porter@v1.3.0/pkg/linter/linter_test.go (about)

     1  package linter
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"get.porter.sh/porter/pkg/config"
     9  	"get.porter.sh/porter/pkg/manifest"
    10  	"get.porter.sh/porter/pkg/mixin"
    11  	"get.porter.sh/porter/pkg/pkgmgmt"
    12  	"get.porter.sh/porter/pkg/portercontext"
    13  	"get.porter.sh/porter/tests"
    14  	"github.com/Masterminds/semver/v3"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestLinter_Lint(t *testing.T) {
    19  	ctx := context.Background()
    20  	testConfig := config.NewTestConfig(t).Config
    21  
    22  	t.Run("no results", func(t *testing.T) {
    23  		cxt := portercontext.NewTestContext(t)
    24  		mixins := mixin.NewTestMixinProvider()
    25  		l := New(cxt.Context, mixins)
    26  		m := &manifest.Manifest{
    27  			Mixins: []manifest.MixinDeclaration{
    28  				{
    29  					Name: "exec",
    30  				},
    31  			},
    32  		}
    33  		mixins.LintResults = nil
    34  
    35  		results, err := l.Lint(ctx, m, testConfig)
    36  		require.NoError(t, err, "Lint failed")
    37  		require.Len(t, results, 0, "linter should have returned 0 results")
    38  	})
    39  
    40  	t.Run("has results", func(t *testing.T) {
    41  		cxt := portercontext.NewTestContext(t)
    42  		mixins := mixin.NewTestMixinProvider()
    43  		l := New(cxt.Context, mixins)
    44  		m := &manifest.Manifest{
    45  			Mixins: []manifest.MixinDeclaration{
    46  				{
    47  					Name: "exec",
    48  				},
    49  			},
    50  		}
    51  		mixins.LintResults = Results{
    52  			{
    53  				Level: LevelWarning,
    54  				Code:  "exec-101",
    55  				Title: "warning stuff isn't working",
    56  			},
    57  		}
    58  
    59  		results, err := l.Lint(ctx, m, testConfig)
    60  		require.NoError(t, err, "Lint failed")
    61  		require.Len(t, results, 1, "linter should have returned 1 result")
    62  		require.Equal(t, mixins.LintResults, results, "unexpected lint results")
    63  	})
    64  
    65  	t.Run("mixin doesn't support lint", func(t *testing.T) {
    66  		cxt := portercontext.NewTestContext(t)
    67  		mixins := mixin.NewTestMixinProvider()
    68  		l := New(cxt.Context, mixins)
    69  		m := &manifest.Manifest{
    70  			Mixins: []manifest.MixinDeclaration{
    71  				{
    72  					Name: "nope",
    73  				},
    74  			},
    75  		}
    76  
    77  		results, err := l.Lint(ctx, m, testConfig)
    78  		require.NoError(t, err, "Lint failed")
    79  		require.Len(t, results, 0, "linter should ignore mixins that doesn't support the lint command")
    80  	})
    81  
    82  	testcases := []struct {
    83  		Name          string
    84  		ParameterName string
    85  	}{
    86  		{
    87  			Name:          "does not use a reserved prefix",
    88  			ParameterName: "porter-debug",
    89  		},
    90  		{
    91  			Name:          "is case insensitive and does not use reserved prefix even if mixed case ",
    92  			ParameterName: "poRteR_lint",
    93  		},
    94  		{
    95  			Name:          "is case insensitive and does not use reserved prefix even if upper case ",
    96  			ParameterName: "PORTER_DEBUG",
    97  		},
    98  	}
    99  
   100  	for _, tc := range testcases {
   101  		t.Run(tc.Name, func(t *testing.T) {
   102  			cxt := portercontext.NewTestContext(t)
   103  			mixins := mixin.NewTestMixinProvider()
   104  			l := New(cxt.Context, mixins)
   105  			param := map[string]manifest.ParameterDefinition{
   106  				"A": {
   107  					Name: tc.ParameterName,
   108  				},
   109  			}
   110  
   111  			m := &manifest.Manifest{
   112  				Parameters: param,
   113  			}
   114  			mixins.LintResults = Results{
   115  				{
   116  					Level: LevelError,
   117  					Location: Location{
   118  						Action:          "",
   119  						Mixin:           "",
   120  						StepNumber:      0,
   121  						StepDescription: "",
   122  					},
   123  					Code:    "porter-100",
   124  					Title:   "Reserved name error",
   125  					Message: tc.ParameterName + " has a reserved prefix. Parameters cannot start with porter- or porter_",
   126  					URL:     "https://porter.sh/reference/linter/#porter-100",
   127  				},
   128  			}
   129  
   130  			results, err := l.Lint(ctx, m, testConfig)
   131  			require.NoError(t, err, "Lint failed")
   132  			require.Len(t, results, 1, "linter should have returned 1 result")
   133  			require.Equal(t, mixins.LintResults, results, "unexpected lint results")
   134  		})
   135  	}
   136  
   137  	t.Run("linter runs successfully if parameter does not use a reserved prefix", func(t *testing.T) {
   138  		cxt := portercontext.NewTestContext(t)
   139  		mixins := mixin.NewTestMixinProvider()
   140  		l := New(cxt.Context, mixins)
   141  		param := map[string]manifest.ParameterDefinition{
   142  			"A": {
   143  				Name: "successful",
   144  			},
   145  		}
   146  
   147  		m := &manifest.Manifest{
   148  			Parameters: param,
   149  		}
   150  		mixins.LintResults = Results{
   151  			{
   152  				Level: LevelError,
   153  				Code:  "exec-101",
   154  				Title: "warning stuff isn't working",
   155  			},
   156  		}
   157  
   158  		results, err := l.Lint(ctx, m, testConfig)
   159  		require.NoError(t, err, "Lint failed")
   160  		require.Len(t, results, 0, "linter should have returned 1 result")
   161  	})
   162  
   163  	t.Run("lint messages does not mention mixins in message not coming from mixin", func(t *testing.T) {
   164  		cxt := portercontext.NewTestContext(t)
   165  		mixins := mixin.NewTestMixinProvider()
   166  		l := New(cxt.Context, mixins)
   167  		param := map[string]manifest.ParameterDefinition{
   168  			"A": {
   169  				Name: "porter_test",
   170  			},
   171  		}
   172  
   173  		m := &manifest.Manifest{
   174  			Parameters: param,
   175  		}
   176  
   177  		results, err := l.Lint(ctx, m, testConfig)
   178  		require.NoError(t, err, "Lint failed")
   179  		require.Len(t, results, 1, "linter should have returned 1 result")
   180  		require.NotContains(t, results[0].String(), ": 0th step in the mixin ()")
   181  	})
   182  }
   183  
   184  func TestLinter_Lint_ParameterDoesNotApplyTo(t *testing.T) {
   185  	ctx := context.Background()
   186  	testCases := []struct {
   187  		action   string
   188  		setSteps func(*manifest.Manifest, manifest.Steps)
   189  	}{
   190  		{"install", func(m *manifest.Manifest, steps manifest.Steps) { m.Install = steps }},
   191  		{"upgrade", func(m *manifest.Manifest, steps manifest.Steps) { m.Upgrade = steps }},
   192  		{"uninstall", func(m *manifest.Manifest, steps manifest.Steps) { m.Uninstall = steps }},
   193  		{"customAction", func(m *manifest.Manifest, steps manifest.Steps) {
   194  			m.CustomActions = make(map[string]manifest.Steps)
   195  			m.CustomActions["customAction"] = steps
   196  		}},
   197  	}
   198  	testConfig := config.NewTestConfig(t).Config
   199  
   200  	for _, tc := range testCases {
   201  		t.Run(tc.action, func(t *testing.T) {
   202  			cxt := portercontext.NewTestContext(t)
   203  			mixins := mixin.NewTestMixinProvider()
   204  			l := New(cxt.Context, mixins)
   205  
   206  			param := map[string]manifest.ParameterDefinition{
   207  				"doesNotApply": {
   208  					Name:    "doesNotApply",
   209  					ApplyTo: []string{"dummy"},
   210  				},
   211  			}
   212  			steps := manifest.Steps{
   213  				&manifest.Step{
   214  					Data: map[string]interface{}{
   215  						"exec": map[string]interface{}{
   216  							"description": "exec step",
   217  							"parameters": []string{
   218  								"\"${ bundle.parameters.doesNotApply }\"",
   219  							},
   220  						},
   221  					},
   222  				},
   223  			}
   224  			m := &manifest.Manifest{
   225  				SchemaVersion:     "1.0.1",
   226  				TemplateVariables: []string{"bundle.parameters.doesNotApply"},
   227  				Parameters:        param,
   228  			}
   229  			tc.setSteps(m, steps)
   230  
   231  			lintResults := Results{
   232  				{
   233  					Level: LevelError,
   234  					Location: Location{
   235  						Action:          tc.action,
   236  						Mixin:           "exec",
   237  						StepNumber:      1,
   238  						StepDescription: "exec step",
   239  					},
   240  					Code:    "porter-101",
   241  					Title:   "Parameter does not apply to action",
   242  					Message: fmt.Sprintf("Parameter doesNotApply does not apply to %s action", tc.action),
   243  					URL:     "https://porter.sh/docs/references/linter/#porter-101",
   244  				},
   245  			}
   246  			results, err := l.Lint(ctx, m, testConfig)
   247  			require.NoError(t, err, "Lint failed")
   248  			require.Len(t, results, 1, "linter should have returned 1 result")
   249  			require.Equal(t, lintResults, results, "unexpected lint results")
   250  		})
   251  	}
   252  }
   253  
   254  func TestLinter_Lint_ParameterAppliesTo(t *testing.T) {
   255  	ctx := context.Background()
   256  	testCases := []struct {
   257  		action   string
   258  		setSteps func(*manifest.Manifest, manifest.Steps)
   259  	}{
   260  		{"install", func(m *manifest.Manifest, steps manifest.Steps) { m.Install = steps }},
   261  		{"upgrade", func(m *manifest.Manifest, steps manifest.Steps) { m.Upgrade = steps }},
   262  		{"uninstall", func(m *manifest.Manifest, steps manifest.Steps) { m.Uninstall = steps }},
   263  		{"customAction", func(m *manifest.Manifest, steps manifest.Steps) {
   264  			m.CustomActions = make(map[string]manifest.Steps)
   265  			m.CustomActions["customAction"] = steps
   266  		}},
   267  	}
   268  	testConfig := config.NewTestConfig(t).Config
   269  
   270  	for _, tc := range testCases {
   271  		t.Run(tc.action, func(t *testing.T) {
   272  			cxt := portercontext.NewTestContext(t)
   273  			mixins := mixin.NewTestMixinProvider()
   274  			l := New(cxt.Context, mixins)
   275  
   276  			param := map[string]manifest.ParameterDefinition{
   277  				"appliesTo": {
   278  					Name:    "appliesTo",
   279  					ApplyTo: []string{tc.action},
   280  				},
   281  			}
   282  			steps := manifest.Steps{
   283  				&manifest.Step{
   284  					Data: map[string]interface{}{
   285  						"exec": map[string]interface{}{
   286  							"description": "exec step",
   287  							"parameters": []string{
   288  								"\"${ bundle.parameters.appliesTo }\"",
   289  							},
   290  						},
   291  					},
   292  				},
   293  			}
   294  			m := &manifest.Manifest{
   295  				SchemaVersion:     "1.0.1",
   296  				TemplateVariables: []string{"bundle.parameters.appliesTo"},
   297  				Parameters:        param,
   298  			}
   299  			tc.setSteps(m, steps)
   300  
   301  			results, err := l.Lint(ctx, m, testConfig)
   302  			require.NoError(t, err, "Lint failed")
   303  			require.Len(t, results, 0, "linter should have returned 1 result")
   304  		})
   305  	}
   306  }
   307  
   308  func TestLinter_DependencyMultipleTimes(t *testing.T) {
   309  	testConfig := config.NewTestConfig(t).Config
   310  
   311  	t.Run("dependency defined multiple times", func(t *testing.T) {
   312  		cxt := portercontext.NewTestContext(t)
   313  		mixins := mixin.NewTestMixinProvider()
   314  		l := New(cxt.Context, mixins)
   315  
   316  		m := &manifest.Manifest{
   317  			Dependencies: manifest.Dependencies{
   318  				Requires: []*manifest.Dependency{
   319  					{Name: "mysql"},
   320  					{Name: "mysql"},
   321  				},
   322  			},
   323  		}
   324  
   325  		expectedResult := Results{
   326  			{
   327  				Code:    "porter-102",
   328  				Title:   "Dependency error",
   329  				Message: "The dependency mysql is defined multiple times",
   330  				URL:     "https://porter.sh/reference/linter/#porter-102",
   331  			},
   332  		}
   333  
   334  		results, err := l.Lint(context.Background(), m, testConfig)
   335  		require.NoError(t, err, "Lint failed")
   336  		require.Len(t, results, 1, "linter should have returned 1 result")
   337  		require.Equal(t, expectedResult, results, "unexpected lint results")
   338  	})
   339  	t.Run("no dependency defined multiple times", func(t *testing.T) {
   340  		cxt := portercontext.NewTestContext(t)
   341  		mixins := mixin.NewTestMixinProvider()
   342  		l := New(cxt.Context, mixins)
   343  
   344  		m := &manifest.Manifest{
   345  			Dependencies: manifest.Dependencies{
   346  				Requires: []*manifest.Dependency{
   347  					{Name: "mysql"},
   348  					{Name: "mongo"},
   349  				},
   350  			},
   351  		}
   352  
   353  		results, err := l.Lint(context.Background(), m, testConfig)
   354  		require.NoError(t, err, "Lint failed")
   355  		require.Len(t, results, 0, "linter should have returned 0 result")
   356  	})
   357  	t.Run("no dependencies", func(t *testing.T) {
   358  		cxt := portercontext.NewTestContext(t)
   359  		mixins := mixin.NewTestMixinProvider()
   360  		l := New(cxt.Context, mixins)
   361  
   362  		m := &manifest.Manifest{}
   363  
   364  		results, err := l.Lint(context.Background(), m, testConfig)
   365  		require.NoError(t, err, "Lint failed")
   366  		require.Len(t, results, 0, "linter should have returned 0 result")
   367  	})
   368  }
   369  
   370  func TestLinter_Lint_MissingMixin(t *testing.T) {
   371  	cxt := portercontext.NewTestContext(t)
   372  	mixins := mixin.NewTestMixinProvider()
   373  	l := New(cxt.Context, mixins)
   374  	testConfig := config.NewTestConfig(t).Config
   375  
   376  	mixinName := "made-up-mixin-that-is-not-installed"
   377  
   378  	m := &manifest.Manifest{
   379  		Mixins: []manifest.MixinDeclaration{
   380  			{
   381  				Name: mixinName,
   382  			},
   383  		},
   384  	}
   385  
   386  	mixins.RunAssertions = append(mixins.RunAssertions, func(mixinCxt *portercontext.Context, mixinName string, commandOpts pkgmgmt.CommandOptions) error {
   387  		return fmt.Errorf("%s not installed", mixinName)
   388  	})
   389  
   390  	_, err := l.Lint(context.Background(), m, testConfig)
   391  	require.Error(t, err, "Linting should return an error")
   392  	tests.RequireOutputContains(t, err.Error(), fmt.Sprintf("%s is not currently installed", mixinName))
   393  }
   394  
   395  func TestLinter_Lint_MixinVersions(t *testing.T) {
   396  	cxt := portercontext.NewTestContext(t)
   397  	mixinProvider := mixin.NewTestMixinProvider()
   398  	l := New(cxt.Context, mixinProvider)
   399  	testConfig := config.NewTestConfig(t).Config
   400  
   401  	exampleMixinVersion := mixin.ExampleMixinSemver.String()
   402  
   403  	// build up some test semvers
   404  	patchDifferenceSemver := fmt.Sprintf("%d.%d.%d", mixin.ExampleMixinSemver.Major(), mixin.ExampleMixinSemver.Minor(), mixin.ExampleMixinSemver.Patch()+1)
   405  	anyPatchAccepted := fmt.Sprintf("%d.%d.x", mixin.ExampleMixinSemver.Major(), mixin.ExampleMixinSemver.Minor())
   406  	lessThanNextMajor := fmt.Sprintf("<%d.%d", mixin.ExampleMixinSemver.Major()+1, mixin.ExampleMixinSemver.Minor())
   407  
   408  	exampleMixinVersionConstraint, _ := semver.NewConstraint(exampleMixinVersion)
   409  	patchDifferenceSemverConstraint, _ := semver.NewConstraint(patchDifferenceSemver)
   410  	anyPatchAcceptedConstraint, _ := semver.NewConstraint(anyPatchAccepted)
   411  	lessThanNextMajorConstraint, _ := semver.NewConstraint(lessThanNextMajor)
   412  
   413  	testCases := []struct {
   414  		name        string
   415  		errExpected bool
   416  		mixins      []manifest.MixinDeclaration
   417  	}{
   418  		{"exact-semver", false, []manifest.MixinDeclaration{
   419  			{
   420  				Name:    mixin.ExampleMixinName,
   421  				Version: exampleMixinVersionConstraint,
   422  			},
   423  		}},
   424  		{"different-patch", true, []manifest.MixinDeclaration{
   425  			{
   426  				Name:    mixin.ExampleMixinName,
   427  				Version: patchDifferenceSemverConstraint,
   428  			},
   429  		}},
   430  		{"accept-different-patch", false, []manifest.MixinDeclaration{
   431  			{
   432  				Name:    mixin.ExampleMixinName,
   433  				Version: anyPatchAcceptedConstraint,
   434  			},
   435  		}},
   436  		{"accept-less-than-versions", false, []manifest.MixinDeclaration{
   437  			{
   438  				Name:    mixin.ExampleMixinName,
   439  				Version: lessThanNextMajorConstraint,
   440  			},
   441  		}},
   442  		{"no-version-provided", false, []manifest.MixinDeclaration{
   443  			{
   444  				Name: mixin.ExampleMixinName,
   445  			},
   446  		}},
   447  	}
   448  
   449  	for _, testCase := range testCases {
   450  		t.Run(testCase.name, func(t *testing.T) {
   451  			m := &manifest.Manifest{
   452  				Mixins: testCase.mixins,
   453  			}
   454  			results, err := l.Lint(context.Background(), m, testConfig)
   455  			if testCase.errExpected {
   456  				require.Error(t, err, "Linting should return an error")
   457  				tests.RequireOutputContains(t, err.Error(), fmt.Sprintf(
   458  					"mixin %s is installed at version v%s but your bundle requires version %s",
   459  					mixin.ExampleMixinName,
   460  					exampleMixinVersion,
   461  					testCase.mixins[0].Version.String(),
   462  				))
   463  			} else {
   464  				require.NoError(t, err, "Linting should not return an error")
   465  			}
   466  			require.Len(t, results, 0, "linter should have returned 0 result")
   467  		})
   468  	}
   469  
   470  }