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