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 }