github.com/ffalor/go-swagger@v0.0.0-20231011000038-9f25265ac351/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  }