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 }