github.com/go-swagger/go-swagger@v0.31.0/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  }