github.com/jfrog/frogbot/v2@v2.21.0/schema/schemas_test.go (about)

     1  package schema
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	clientutils "github.com/jfrog/jfrog-client-go/utils"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/xeipuuv/gojsonschema"
    16  	"gopkg.in/yaml.v3"
    17  )
    18  
    19  const (
    20  	maxRetriesToDownloadSchema           = 5
    21  	durationBetweenSchemaDownloadRetries = 10 * time.Second
    22  )
    23  
    24  func TestFrogbotSchema(t *testing.T) {
    25  	// Load frogbot schema
    26  	schema, err := os.ReadFile("frogbot-schema.json")
    27  	assert.NoError(t, err)
    28  	schemaLoader := gojsonschema.NewBytesLoader(schema)
    29  
    30  	// Validate config in this repository
    31  	validateYamlSchema(t, schemaLoader, filepath.Join("..", ".frogbot", "frogbot-config.yml"), "")
    32  
    33  	// Validate all frogbot configs in commands/testdata/config
    34  	validateYamlsInDirectory(t, filepath.Join("..", "testdata", "config"), schemaLoader)
    35  }
    36  
    37  func TestBadFrogbotSchemas(t *testing.T) {
    38  	// Load frogbot schema
    39  	schema, err := os.ReadFile("frogbot-schema.json")
    40  	assert.NoError(t, err)
    41  	schemaLoader := gojsonschema.NewBytesLoader(schema)
    42  
    43  	// Validate all bad frogbot configs in schema/testdata/
    44  	testCases := []struct {
    45  		testName    string
    46  		errorString string
    47  	}{
    48  		{"additional-prop", "Additional property additionalProp is not allowed"},
    49  		{"no-array", "Expected: array, given: object"},
    50  		{"no-git", "git is required"},
    51  		{"no-repo", "repoName is required"},
    52  		{"empty-repo", "Expected: string, given: null"},
    53  	}
    54  	for _, testCase := range testCases {
    55  		validateYamlSchema(t, schemaLoader, filepath.Join("testdata", testCase.testName+".yml"), testCase.errorString)
    56  	}
    57  }
    58  
    59  func TestJFrogPipelinesTemplates(t *testing.T) {
    60  	schemaLoader := downloadFromSchemaStore(t, "jfrog-pipelines.json")
    61  	validateYamlsInDirectory(t, filepath.Join("..", "docs", "templates", "jfrog-pipelines"), schemaLoader)
    62  }
    63  
    64  // Download a Yaml schema from https://json.schemastore.org.
    65  // t      - Testing object
    66  // schema - The schema file to download
    67  func downloadFromSchemaStore(t *testing.T, schema string) gojsonschema.JSONLoader {
    68  	var response *http.Response
    69  	var err error
    70  	retryExecutor := clientutils.RetryExecutor{
    71  		MaxRetries:               maxRetriesToDownloadSchema,
    72  		RetriesIntervalMilliSecs: int(durationBetweenSchemaDownloadRetries.Milliseconds()),
    73  		ErrorMessage:             "Failed to download schema.",
    74  		ExecutionHandler: func() (bool, error) {
    75  			response, err = http.Get("https://json.schemastore.org/" + schema)
    76  			if err != nil {
    77  				return true, err
    78  			}
    79  			if response.StatusCode != http.StatusOK {
    80  				return true, fmt.Errorf("failed to download schema. Response status: %s", response.Status)
    81  			}
    82  			return false, nil
    83  		},
    84  	}
    85  	assert.NoError(t, retryExecutor.Execute())
    86  	assert.Equal(t, http.StatusOK, response.StatusCode, response.Status)
    87  	// Check server response and read schema bytes
    88  	defer func() {
    89  		assert.NoError(t, response.Body.Close())
    90  	}()
    91  	schemaBytes, err := io.ReadAll(response.Body)
    92  	assert.NoError(t, err)
    93  	return gojsonschema.NewBytesLoader(schemaBytes)
    94  }
    95  
    96  // Validate all yml files in the given directory against the input schema
    97  // t            - Testing object
    98  // schemaLoader - Frogbot config schema
    99  // path	         - Yaml directory path
   100  func validateYamlsInDirectory(t *testing.T, path string, schemaLoader gojsonschema.JSONLoader) {
   101  	err := filepath.Walk(path, func(frogbotConfigFilePath string, info os.FileInfo, err error) error {
   102  		assert.NoError(t, err)
   103  		if strings.HasSuffix(info.Name(), "yml") {
   104  			validateYamlSchema(t, schemaLoader, frogbotConfigFilePath, "")
   105  		}
   106  		return nil
   107  	})
   108  	assert.NoError(t, err)
   109  }
   110  
   111  // Validate a Yaml file against the input Yaml schema
   112  // t            - Testing object
   113  // schemaLoader - Frogbot config schema
   114  // yamlFilePath - Yaml file path
   115  // expectError  - Expected error or an empty string if error is not expected
   116  func validateYamlSchema(t *testing.T, schemaLoader gojsonschema.JSONLoader, yamlFilePath, expectError string) {
   117  	t.Run(filepath.Base(yamlFilePath), func(t *testing.T) {
   118  		// Read frogbot config
   119  		yamlFile, err := os.ReadFile(yamlFilePath)
   120  		assert.NoError(t, err)
   121  
   122  		// Unmarshal frogbot config
   123  		var frogbotConfigYaml interface{}
   124  		err = yaml.Unmarshal(yamlFile, &frogbotConfigYaml)
   125  		assert.NoError(t, err)
   126  
   127  		// Convert the Yaml config to JSON config to help the json parser validate it.
   128  		// The reason we don't do the convert by as follows:
   129  		// YAML -> Unmarshall -> Go Struct -> Marshal -> JSON
   130  		// is because the config's struct includes only YAML annotations.
   131  		frogbotConfigJson := convertYamlToJson(frogbotConfigYaml)
   132  
   133  		// Load and validate frogbot config
   134  		documentLoader := gojsonschema.NewGoLoader(frogbotConfigJson)
   135  		result, err := gojsonschema.Validate(schemaLoader, documentLoader)
   136  		assert.NoError(t, err)
   137  		if expectError != "" {
   138  			assert.False(t, result.Valid())
   139  			assert.Contains(t, result.Errors()[0].String(), expectError)
   140  		} else {
   141  			assert.True(t, result.Valid(), result.Errors())
   142  		}
   143  	})
   144  }
   145  
   146  // Recursively convert yaml interface to JSON interface
   147  func convertYamlToJson(yamlValue interface{}) interface{} {
   148  	switch yamlMapping := yamlValue.(type) {
   149  	case map[interface{}]interface{}:
   150  		jsonMapping := map[string]interface{}{}
   151  		for key, value := range yamlMapping {
   152  			if key == true {
   153  				// "on" is considered a true value for the Yaml Unmarshaler. To work around it, we set the true to be "on".
   154  				key = "on"
   155  			}
   156  			jsonMapping[fmt.Sprint(key)] = convertYamlToJson(value)
   157  		}
   158  		return jsonMapping
   159  	case []interface{}:
   160  		for i, value := range yamlMapping {
   161  			yamlMapping[i] = convertYamlToJson(value)
   162  		}
   163  	}
   164  	return yamlValue
   165  }