github.com/w3security/vervet/v5@v5.3.1-0.20230618081846-5bd9b5d799dc/internal/compiler/compiler_test.go (about)

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