github.com/snyk/vervet/v4@v4.27.2/internal/compiler/compiler_test.go (about)

     1  package compiler
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io/fs"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"testing"
    11  	"text/template"
    12  
    13  	qt "github.com/frankban/quicktest"
    14  
    15  	"github.com/snyk/vervet/v4/config"
    16  	"github.com/snyk/vervet/v4/internal/files"
    17  	"github.com/snyk/vervet/v4/internal/linter"
    18  	"github.com/snyk/vervet/v4/testdata"
    19  )
    20  
    21  func setup(c *qt.C) {
    22  	c.Setenv("API_BASE_URL", "https://example.com/api/rest")
    23  	cwd, err := os.Getwd()
    24  	c.Assert(err, qt.IsNil)
    25  	err = os.Chdir(testdata.Path(".."))
    26  	c.Assert(err, qt.IsNil)
    27  	c.Cleanup(func() {
    28  		err := os.Chdir(cwd)
    29  		c.Assert(err, qt.IsNil)
    30  	})
    31  }
    32  
    33  var configTemplate = template.Must(template.New("vervet.yaml").Parse(`
    34  linters:
    35    resource-rules:
    36      spectral:
    37        rules:
    38          - 'node_modules/@snyk/sweater-comb/resource.yaml'
    39    compiled-rules:
    40      spectral:
    41        rules:
    42          - 'node_modules/@snyk/sweater-comb/compiled.yaml'
    43  apis:
    44    rest-api:
    45      resources:
    46        - linter: resource-rules
    47          path: 'testdata/resources'
    48          excludes:
    49            - 'testdata/resources/schemas/**'
    50      overlays:
    51        - include: 'testdata/resources/include.yaml'
    52        - inline: |-
    53            servers:
    54              - url: ${API_BASE_URL}
    55                description: Test REST API
    56      output:
    57        path: {{ . }}
    58        linter: compiled-rules
    59  `[1:]))
    60  
    61  var configTemplateWithPaths = template.Must(template.New("vervet.yaml").Parse(`
    62  linters:
    63    resource-rules:
    64      spectral:
    65        rules:
    66          - 'node_modules/@snyk/sweater-comb/resource.yaml'
    67    compiled-rules:
    68      spectral:
    69        rules:
    70          - 'node_modules/@snyk/sweater-comb/compiled.yaml'
    71  apis:
    72    rest-api:
    73      resources:
    74        - linter: resource-rules
    75          path: 'testdata/resources'
    76          excludes:
    77            - 'testdata/resources/schemas/**'
    78      overlays:
    79        - include: 'testdata/resources/include.yaml'
    80        - inline: |-
    81            servers:
    82              - url: ${API_BASE_URL}
    83                description: Test REST API
    84      output:
    85        paths:
    86  {{- range . }}
    87          - {{ . }}
    88  {{- end }}
    89        linter: compiled-rules
    90  `[1:]))
    91  
    92  // Sanity-check the compiler at lifecycle stages in a simple scenario. This
    93  // isn't meant to be a comprehensive end-to-end test of the compiler; those are
    94  // done with fixtures. These are easier to break out, debug, and add specific
    95  // asserts when the e2e fixtures fail.
    96  func TestCompilerSmoke(t *testing.T) {
    97  	c := qt.New(t)
    98  	setup(c)
    99  	ctx := context.Background()
   100  	outputPath := c.TempDir()
   101  	var configBuf bytes.Buffer
   102  	err := configTemplate.Execute(&configBuf, outputPath)
   103  	c.Assert(err, qt.IsNil)
   104  
   105  	// Create a file that should be removed prior to build
   106  	err = ioutil.WriteFile(outputPath+"/goof", []byte("goof"), 0777)
   107  	c.Assert(err, qt.IsNil)
   108  
   109  	proj, err := config.Load(bytes.NewBuffer(configBuf.Bytes()))
   110  	c.Assert(err, qt.IsNil)
   111  	compiler, err := New(ctx, proj, LinterFactory(func(context.Context, *config.Linter) (linter.Linter, error) {
   112  		return &mockLinter{}, nil
   113  	}))
   114  	c.Assert(err, qt.IsNil)
   115  
   116  	// Assert constructor set things up as expected
   117  	c.Assert(compiler.apis, qt.HasLen, 1)
   118  	c.Assert(compiler.linters, qt.HasLen, 2)
   119  	restApi := compiler.apis["rest-api"]
   120  	c.Assert(restApi, qt.Not(qt.IsNil))
   121  	c.Assert(restApi.resources, qt.HasLen, 1)
   122  	c.Assert(restApi.resources[0].sourceFiles, qt.Contains, "testdata/resources/projects/2021-06-04/spec.yaml")
   123  	c.Assert(restApi.overlayIncludes, qt.HasLen, 1)
   124  	c.Assert(restApi.overlayIncludes[0].Paths, qt.HasLen, 2)
   125  	c.Assert(restApi.overlayInlines[0].Servers[0].URL, qt.Contains, "https://example.com/api/rest", qt.Commentf("environment variable interpolation"))
   126  	c.Assert(restApi.output, qt.Not(qt.IsNil))
   127  
   128  	// LintResources stage
   129  	err = compiler.LintResourcesAll(ctx)
   130  	c.Assert(err, qt.IsNil)
   131  	c.Assert(compiler.linters["resource-rules"].(*mockLinter).runs, qt.HasLen, 1)
   132  	c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs, qt.HasLen, 0)
   133  	c.Assert(compiler.linters["resource-rules"].(*mockLinter).runs[0], qt.Contains, "testdata/resources/projects/2021-06-04/spec.yaml")
   134  
   135  	// Build stage
   136  	err = compiler.BuildAll(ctx)
   137  	c.Assert(err, qt.IsNil)
   138  
   139  	// Verify created files/folders are as expected
   140  	// Look for existence of /2021-06-01~experimental
   141  	refOutputPath := testdata.Path("output")
   142  	assertOutputsEqual(c, refOutputPath, outputPath)
   143  
   144  	// Look for absence of /2021-06-01 folder (ga)
   145  	_, err = os.Stat(outputPath + "/2021-06-01")
   146  	c.Assert(os.IsNotExist(err), qt.IsTrue)
   147  
   148  	// Build output was cleaned up
   149  	_, err = ioutil.ReadFile(outputPath + "/goof")
   150  	c.Assert(err, qt.ErrorMatches, ".*/goof: no such file or directory")
   151  
   152  	// LintOutput stage
   153  	err = compiler.LintOutputAll(ctx)
   154  	c.Assert(err, qt.IsNil)
   155  	c.Assert(compiler.linters["resource-rules"].(*mockLinter).runs, qt.HasLen, 1)
   156  	c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs, qt.HasLen, 1)
   157  	c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs[0], qt.Contains, outputPath+"/2021-06-04~experimental/spec.yaml")
   158  	c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs[0], qt.Contains, outputPath+"/2021-06-04~experimental/spec.json")
   159  }
   160  
   161  func TestCompilerSmokePaths(t *testing.T) {
   162  	c := qt.New(t)
   163  	setup(c)
   164  	ctx := context.Background()
   165  	outputPaths := []string{c.TempDir(), c.TempDir()}
   166  	var configBuf bytes.Buffer
   167  	err := configTemplateWithPaths.Execute(&configBuf, outputPaths)
   168  	c.Assert(err, qt.IsNil)
   169  
   170  	// Create a file that should be removed prior to build
   171  	err = ioutil.WriteFile(outputPaths[0]+"/goof", []byte("goof"), 0777)
   172  	c.Assert(err, qt.IsNil)
   173  
   174  	proj, err := config.Load(bytes.NewBuffer(configBuf.Bytes()))
   175  	c.Assert(err, qt.IsNil)
   176  	compiler, err := New(ctx, proj, LinterFactory(func(context.Context, *config.Linter) (linter.Linter, error) {
   177  		return &mockLinter{}, nil
   178  	}))
   179  	c.Assert(err, qt.IsNil)
   180  
   181  	// Build stage
   182  	err = compiler.BuildAll(ctx)
   183  	c.Assert(err, qt.IsNil)
   184  
   185  	refOutputPath := testdata.Path("output")
   186  	// Verify created files/folders are as expected
   187  	for _, outputPath := range outputPaths {
   188  		assertOutputsEqual(c, refOutputPath, outputPath)
   189  
   190  		// Build output was cleaned up
   191  		_, err = ioutil.ReadFile(outputPath + "/goof")
   192  		c.Assert(err, qt.ErrorMatches, ".*/goof: no such file or directory")
   193  	}
   194  
   195  	// LintOutput stage
   196  	// Only the first output path is linted, others are copies
   197  	err = compiler.LintOutputAll(ctx)
   198  	c.Assert(err, qt.IsNil)
   199  	c.Assert(compiler.linters["resource-rules"].(*mockLinter).runs, qt.HasLen, 0)
   200  	c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs, qt.HasLen, 1)
   201  	c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs[0], qt.Contains, outputPaths[0]+"/2021-06-04~experimental/spec.yaml")
   202  	c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs[0], qt.Contains, outputPaths[0]+"/2021-06-04~experimental/spec.json")
   203  }
   204  
   205  func assertOutputsEqual(c *qt.C, refDir, testDir string) {
   206  	err := fs.WalkDir(os.DirFS(refDir), ".", func(path string, d fs.DirEntry, err error) error {
   207  		c.Assert(err, qt.IsNil)
   208  		if d.IsDir() {
   209  			return nil
   210  		}
   211  		if d.Name() != "spec.yaml" {
   212  			// only comparing compiled specs here
   213  			return nil
   214  		}
   215  		outputFile, err := os.ReadFile(filepath.Join(testDir, path))
   216  		c.Assert(err, qt.IsNil)
   217  		refFile, err := os.ReadFile(filepath.Join(refDir, path))
   218  		c.Assert(err, qt.IsNil)
   219  		c.Assert(string(outputFile), qt.Equals, string(refFile), qt.Commentf("%s", path))
   220  		return nil
   221  	})
   222  	c.Assert(err, qt.IsNil)
   223  }
   224  
   225  type mockLinter struct {
   226  	runs     [][]string
   227  	override *config.Linter
   228  	err      error
   229  }
   230  
   231  func (l *mockLinter) Match(rcConfig *config.ResourceSet) ([]string, error) {
   232  	return files.LocalFSSource{}.Match(rcConfig)
   233  }
   234  
   235  func (l *mockLinter) Run(ctx context.Context, root string, paths ...string) error {
   236  	l.runs = append(l.runs, paths)
   237  	return l.err
   238  }
   239  
   240  func (l *mockLinter) WithOverride(ctx context.Context, cfg *config.Linter) (linter.Linter, error) {
   241  	nl := &mockLinter{
   242  		override: cfg,
   243  	}
   244  	return nl, nil
   245  }