github.com/josephspurrier/go-swagger@v0.2.1-0.20221129144919-1f672a142a00/generator/moreschemavalidation_test.go (about) 1 // Copyright 2015 go-swagger maintainers 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package generator 16 17 import ( 18 "bytes" 19 "io" 20 "log" 21 "os" 22 "path" 23 "strings" 24 "sync" 25 "testing" 26 27 "github.com/go-openapi/loads" 28 "github.com/go-openapi/spec" 29 "github.com/go-openapi/swag" 30 "github.com/stretchr/testify/assert" 31 "github.com/stretchr/testify/require" 32 ) 33 34 // modelExpectations is a test structure to capture expected codegen lines of code 35 type modelExpectations struct { 36 GeneratedFile string 37 ExpectedLines []string 38 NotExpectedLines []string 39 ExpectedLogs []string 40 NotExpectedLogs []string 41 ExpectFailure bool 42 } 43 44 func (m modelExpectations) ExpectLogs() bool { 45 // does this test case assert output? 46 return len(m.ExpectedLogs) > 0 || len(m.NotExpectedLogs) > 0 47 } 48 49 func (m modelExpectations) AssertModelLogs(t testing.TB, msg, definitionName, fixtureSpec string) { 50 // assert logged output 51 for line, logLine := range m.ExpectedLogs { 52 if !assertInCode(t, strings.TrimSpace(logLine), msg) { 53 t.Logf("log expected did not match for definition %q in fixture %s at (fixture) log line %d", definitionName, fixtureSpec, line) 54 } 55 } 56 57 for line, logLine := range m.NotExpectedLogs { 58 if !assertNotInCode(t, strings.TrimSpace(logLine), msg) { 59 t.Logf("log unexpectedly matched for definition %q in fixture %s at (fixture) log line %d", definitionName, fixtureSpec, line) 60 } 61 } 62 63 if t.Failed() { 64 t.FailNow() 65 } 66 } 67 68 func (m modelExpectations) AssertModelCodegen(t testing.TB, msg, definitionName, fixtureSpec string) { 69 // assert generated code 70 for line, codeLine := range m.ExpectedLines { 71 if !assertInCode(t, strings.TrimSpace(codeLine), msg) { 72 t.Logf("code expected did not match for definition %q in fixture %s at (fixture) line %d", definitionName, fixtureSpec, line) 73 } 74 } 75 76 for line, codeLine := range m.NotExpectedLines { 77 if !assertNotInCode(t, strings.TrimSpace(codeLine), msg) { 78 t.Logf("code unexpectedly matched for definition %q in fixture %s at (fixture) line %d", definitionName, fixtureSpec, line) 79 } 80 } 81 82 if t.Failed() { 83 t.FailNow() 84 } 85 } 86 87 // modelTestRun is a test structure to configure generations options to test a spec 88 type modelTestRun struct { 89 FixtureOpts *GenOpts 90 Definitions map[string]*modelExpectations 91 } 92 93 // AddExpectations adds expected / not expected sets of lines of code to the current run 94 func (r *modelTestRun) AddExpectations(file string, expectedCode, notExpectedCode, expectedLogs, notExpectedLogs []string) { 95 k := strings.ToLower(swag.ToJSONName(strings.TrimSuffix(file, ".go"))) 96 if def, ok := r.Definitions[k]; ok { 97 def.ExpectedLines = append(def.ExpectedLines, expectedCode...) 98 def.NotExpectedLines = append(def.NotExpectedLines, notExpectedCode...) 99 def.ExpectedLogs = append(def.ExpectedLogs, expectedLogs...) 100 def.NotExpectedLogs = append(def.NotExpectedLogs, notExpectedLogs...) 101 return 102 } 103 104 r.Definitions[k] = &modelExpectations{ 105 GeneratedFile: file, 106 ExpectedLines: expectedCode, 107 NotExpectedLines: notExpectedCode, 108 ExpectedLogs: expectedLogs, 109 NotExpectedLogs: notExpectedLogs, 110 } 111 } 112 113 // ExpectedFor returns the map of model expectations from the run for a given model definition 114 func (r *modelTestRun) ExpectedFor(definition string) *modelExpectations { 115 if def, ok := r.Definitions[strings.ToLower(definition)]; ok { 116 return def 117 } 118 return nil 119 } 120 121 func (r *modelTestRun) WithMinimalFlatten(minimal bool) *modelTestRun { 122 r.FixtureOpts.FlattenOpts.Minimal = minimal 123 return r 124 } 125 126 // modelFixture is a test structure to launch configurable test runs on a given spec 127 type modelFixture struct { 128 SpecFile string 129 Description string 130 Runs []*modelTestRun 131 } 132 133 // Add adds a new run to the provided model fixture 134 func (f *modelFixture) AddRun(expandSpec bool) *modelTestRun { 135 opts := &GenOpts{} 136 opts.IncludeValidator = true 137 opts.IncludeModel = true 138 opts.ValidateSpec = false 139 opts.Spec = f.SpecFile 140 if err := opts.EnsureDefaults(); err != nil { 141 panic(err) 142 } 143 144 // sets gen options (e.g. flatten vs expand) - full flatten is the default setting for this test (NOT the default CLI option!) 145 opts.FlattenOpts.Expand = expandSpec 146 opts.FlattenOpts.Minimal = false 147 148 defs := make(map[string]*modelExpectations, 150) 149 run := &modelTestRun{ 150 FixtureOpts: opts, 151 Definitions: defs, 152 } 153 f.Runs = append(f.Runs, run) 154 return run 155 } 156 157 // ExpectedBy returns the expectations from another run of the current fixture, recalled by its index in the list of planned runs 158 func (f *modelFixture) ExpectedFor(index int, definition string) *modelExpectations { 159 if index > len(f.Runs)-1 { 160 return nil 161 } 162 if def, ok := f.Runs[index].Definitions[strings.ToLower(definition)]; ok { 163 return def 164 } 165 return nil 166 } 167 168 // newModelFixture is a test utility to build a new test plan for a spec file. 169 // The returned structure may be then used to add runs and expectations to each run. 170 func newModelFixture(specFile string, description string) *modelFixture { 171 // lookup if already here 172 for _, fix := range testedModels { 173 if fix.SpecFile == specFile { 174 return fix 175 } 176 } 177 runs := make([]*modelTestRun, 0, 2) 178 fix := &modelFixture{ 179 SpecFile: specFile, 180 Description: description, 181 Runs: runs, 182 } 183 testedModels = append(testedModels, fix) 184 return fix 185 } 186 187 // all tested specs: init at the end of this source file 188 // you may append to those with different initXXX() funcs below. 189 var ( 190 modelTestMutex = &sync.Mutex{} // mutex to protect log capture 191 testedModels []*modelFixture 192 193 // convenient vars for (not) matching some lines 194 noLines []string 195 todo []string 196 validatable []string 197 warning []string 198 ) 199 200 func initSchemaValidationTest() { 201 testedModels = make([]*modelFixture, 0, 50) 202 noLines = []string{} 203 todo = []string{`TODO`} 204 validatable = append([]string{`Validate(`}, todo...) 205 warning = []string{`warning`} 206 } 207 208 // initModelFixtures loads all tests to be performed 209 func initModelFixtures() { 210 initFixtureSimpleAllOf() 211 initFixtureComplexAllOf() 212 initFixtureIsNullable() 213 initFixtureItching() 214 initFixtureAdditionalProps() 215 initFixtureTuple() 216 initFixture1479Part() 217 initFixture1198() 218 initFixture1042() 219 initFixture1042V2() 220 initFixture979() 221 initFixture842() 222 initFixture607() 223 initFixture1336() 224 initFixtureErrors() 225 initFixture844Variations() 226 initFixtureMoreAddProps() 227 // a more stringent verification of this known fixture 228 initTodolistSchemavalidation() 229 initFixture1537() 230 initFixture1537v2() 231 232 // more maps and nullability checks 233 initFixture15365() 234 initFixtureNestedMaps() 235 initFixtureDeepMaps() 236 237 // format "byte" validation 238 initFixture1548() 239 240 // more tuples 241 initFixtureSimpleTuple() 242 243 // allOf with properties 244 initFixture1617() 245 246 // type realiasing 247 initFixtureRealiasedTypes() 248 249 // required base type 250 initFixture1993() 251 252 // allOf marshallers 253 initFixture2071() 254 255 // x-omitempty 256 initFixture2116() 257 258 // additionalProperties in base type (pending fix, non regression assertion only atm) 259 initFixture2220() 260 261 // allOf can be forced to non-nullable 262 initFixture2364() 263 264 // ReadOnly ContextValidate 265 initFixture936ReadOnly() 266 267 // required interface{} 268 initFixture2081() 269 270 // required map 271 initFixture2300() 272 273 // required $ref primitive 274 initFixture2381() 275 276 // required aliased primitive 277 initFixture2400() 278 279 // numerical validations 280 initFixture2448() 281 282 initFixtureGuardFormats() 283 284 // min / maxProperties 285 initFixture2444() 286 287 // map of nullable array 288 initFixture2494() 289 } 290 291 /* Template initTxxx() to prepare and load a fixture: 292 293 func initTxxx() { 294 // testing xxx.yaml with expand (--with-expand) 295 f := newModelFixture("xxx.yaml", "A test blg") 296 297 // makes a run with expandSpec=false (full flattening) 298 thisRun := f.AddRun(false) 299 300 // loads expectations for model abc 301 thisRun.AddExpectations("abc.go", []string{ 302 `line {`, 303 ` more codegen `, 304 `}`, 305 }, 306 // not expected 307 noLines, 308 // output in Log 309 noLines, 310 noLines) 311 312 // loads expectations for model abcDef 313 thisRun.AddExpectations("abc_def.go", []string{}, []string{}, noLines, noLines) 314 } 315 316 */ 317 318 func TestModelGenerateDefinition(t *testing.T) { 319 // exercise the top level model generation func 320 log.SetOutput(io.Discard) 321 defer func() { 322 log.SetOutput(os.Stdout) 323 }() 324 fixtureSpec := "../fixtures/bugs/1487/fixture-is-nullable.yaml" 325 assert := assert.New(t) 326 gendir, erd := os.MkdirTemp(".", "model-test") 327 defer func() { 328 _ = os.RemoveAll(gendir) 329 }() 330 require.NoError(t, erd) 331 332 opts := &GenOpts{} 333 opts.IncludeValidator = true 334 opts.IncludeModel = true 335 opts.ValidateSpec = false 336 opts.Spec = fixtureSpec 337 opts.ModelPackage = "models" 338 opts.Target = gendir 339 if err := opts.EnsureDefaults(); err != nil { 340 panic(err) 341 } 342 // sets gen options (e.g. flatten vs expand) - flatten is the default setting 343 opts.FlattenOpts.Minimal = false 344 345 err := GenerateDefinition([]string{"thingWithNullableDates"}, opts) 346 assert.NoErrorf(err, "Expected GenerateDefinition() to run without error") 347 348 err = GenerateDefinition(nil, opts) 349 assert.NoErrorf(err, "Expected GenerateDefinition() to run without error") 350 351 opts.TemplateDir = gendir 352 err = GenerateDefinition([]string{"thingWithNullableDates"}, opts) 353 assert.NoErrorf(err, "Expected GenerateDefinition() to run without error") 354 355 err = GenerateDefinition([]string{"thingWithNullableDates"}, nil) 356 assert.Errorf(err, "Expected GenerateDefinition() return an error when no option is passed") 357 358 opts.TemplateDir = "templates" 359 err = GenerateDefinition([]string{"thingWithNullableDates"}, opts) 360 assert.Errorf(err, "Expected GenerateDefinition() to croak about protected templates") 361 362 opts.TemplateDir = "" 363 err = GenerateDefinition([]string{"myAbsentDefinition"}, opts) 364 assert.Errorf(err, "Expected GenerateDefinition() to return an error when the model is not in spec") 365 366 opts.Spec = "pathToNowhere" 367 err = GenerateDefinition([]string{"thingWithNullableDates"}, opts) 368 assert.Errorf(err, "Expected GenerateDefinition() to return an error when the spec is not reachable") 369 } 370 371 func TestMoreModelValidations(t *testing.T) { 372 log.SetOutput(io.Discard) 373 defer func() { 374 log.SetOutput(os.Stdout) 375 }() 376 377 initModelFixtures() 378 379 t.Logf("INFO: model specs tested: %d", len(testedModels)) 380 for _, toPin := range testedModels { 381 fixture := toPin 382 if fixture.SpecFile == "" { 383 continue 384 } 385 fixtureSpec := fixture.SpecFile 386 runTitle := strings.Join([]string{"codegen", strings.TrimSuffix(path.Base(fixtureSpec), path.Ext(fixtureSpec))}, "-") 387 388 t.Run(runTitle, func(t *testing.T) { 389 t.Parallel() 390 log.SetOutput(io.Discard) 391 392 for _, fixtureRun := range fixture.Runs { 393 opts := fixtureRun.FixtureOpts 394 opts.Spec = fixtureSpec 395 // this is the expanded or flattened spec 396 newSpecDoc, err := opts.validateAndFlattenSpec() 397 require.NoErrorf(t, err, "could not expand/flatten fixture %s: %v", fixtureSpec, err) 398 399 definitions := newSpecDoc.Spec().Definitions 400 for k, fixtureExpectations := range fixtureRun.Definitions { 401 // pick definition to test 402 definitionName, schema := findTestDefinition(k, definitions) 403 require.NotNilf(t, schema, "expected to find definition %q in model fixture %s", k, fixtureSpec) 404 405 checkDefinitionCodegen(t, definitionName, fixtureSpec, schema, newSpecDoc, opts, fixtureExpectations) 406 } 407 } 408 }) 409 } 410 } 411 412 func findTestDefinition(k string, definitions spec.Definitions) (string, *spec.Schema) { 413 var ( 414 schema *spec.Schema 415 definitionName string 416 ) 417 418 for def, s := range definitions { 419 // please do not inject fixtures with case conflicts on defs... 420 // this one is just easier to retrieve model back from file names when capturing 421 // the generated code. 422 mangled := swag.ToJSONName(def) 423 if strings.EqualFold(mangled, k) { 424 definition := s 425 schema = &definition 426 definitionName = def 427 break 428 } 429 } 430 return definitionName, schema 431 } 432 433 func checkDefinitionCodegen(t testing.TB, definitionName, fixtureSpec string, schema *spec.Schema, specDoc *loads.Document, opts *GenOpts, fixtureExpectations *modelExpectations) { 434 // prepare assertions on log output (e.g. generation warnings) 435 var logCapture bytes.Buffer 436 var msg string 437 438 if fixtureExpectations.ExpectLogs() { 439 // lock when capturing shared log resource (hopefully not for all testcases) 440 modelTestMutex.Lock() 441 log.SetOutput(&logCapture) 442 443 defer func() { 444 log.SetOutput(io.Discard) 445 modelTestMutex.Unlock() 446 }() 447 } 448 449 // generate the schema for this definition 450 genModel, err := makeGenDefinition(definitionName, "models", *schema, specDoc, opts) 451 if fixtureExpectations.ExpectLogs() { 452 msg = logCapture.String() 453 } 454 455 if fixtureExpectations.ExpectFailure { 456 // expected an error here, and it has not happened 457 require.Errorf(t, err, "Expected an error during generation of definition %q from spec fixture %s", definitionName, fixtureSpec) 458 } 459 460 // expected smooth generation 461 require.NoErrorf(t, err, "could not generate model definition %q from spec fixture %s: %v", definitionName, fixtureSpec, err) 462 463 if fixtureExpectations.ExpectLogs() { 464 fixtureExpectations.AssertModelLogs(t, msg, definitionName, fixtureSpec) 465 } 466 467 // execute the model template with this schema 468 buf := bytes.NewBuffer(nil) 469 err = templates.MustGet("model").Execute(buf, genModel) 470 require.NoErrorf(t, err, "could not render model template for definition %q in spec fixture %s: %v", definitionName, fixtureSpec, err) 471 472 outputName := fixtureExpectations.GeneratedFile 473 if outputName == "" { 474 outputName = swag.ToFileName(definitionName) + ".go" 475 } 476 477 // run goimport, gofmt on the generated code 478 formatted, err := opts.LanguageOpts.FormatContent(outputName, buf.Bytes()) 479 require.NoErrorf(t, err, "could not render model template for definition %q in spec fixture %s: %v", definitionName, fixtureSpec, err) 480 481 // assert generated code (see fixture file) 482 fixtureExpectations.AssertModelCodegen(t, string(formatted), definitionName, fixtureSpec) 483 }