github.com/googleapis/api-linter@v1.65.2/lint/rule_test.go (about)

     1  // Copyright 2019 Google LLC
     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  // 		https://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 lint
    16  
    17  import (
    18  	"reflect"
    19  	"testing"
    20  
    21  	"github.com/jhump/protoreflect/desc"
    22  	"github.com/jhump/protoreflect/desc/builder"
    23  )
    24  
    25  func TestFileRule(t *testing.T) {
    26  	// Create a file descriptor with nothing in it.
    27  	fd, err := builder.NewFile("test.proto").Build()
    28  	if err != nil {
    29  		t.Fatalf("Could not build file descriptor: %q", err)
    30  	}
    31  
    32  	// Iterate over the tests and run them.
    33  	for _, test := range makeLintRuleTests(fd) {
    34  		t.Run(test.testName, func(t *testing.T) {
    35  			rule := &FileRule{
    36  				Name: RuleName("test"),
    37  				OnlyIf: func(fd *desc.FileDescriptor) bool {
    38  					return fd.GetName() == "test.proto"
    39  				},
    40  				LintFile: func(fd *desc.FileDescriptor) []Problem {
    41  					return test.problems
    42  				},
    43  			}
    44  
    45  			// Run the rule and assert that we got what we expect.
    46  			test.runRule(rule, fd, t)
    47  		})
    48  	}
    49  }
    50  
    51  func TestMessageRule(t *testing.T) {
    52  	// Create a file descriptor with two messages in it.
    53  	fd, err := builder.NewFile("test.proto").AddMessage(
    54  		builder.NewMessage("Book"),
    55  	).AddMessage(
    56  		builder.NewMessage("Author"),
    57  	).Build()
    58  	if err != nil {
    59  		t.Fatalf("Failed to build file descriptor.")
    60  	}
    61  
    62  	// Iterate over the tests and run them.
    63  	for _, test := range makeLintRuleTests(fd.GetMessageTypes()[1]) {
    64  		t.Run(test.testName, func(t *testing.T) {
    65  			// Create the message rule.
    66  			rule := &MessageRule{
    67  				Name: RuleName("test"),
    68  				OnlyIf: func(m *desc.MessageDescriptor) bool {
    69  					return m.GetName() == "Author"
    70  				},
    71  				LintMessage: func(m *desc.MessageDescriptor) []Problem {
    72  					return test.problems
    73  				},
    74  			}
    75  
    76  			// Run the rule and assert that we got what we expect.
    77  			test.runRule(rule, fd, t)
    78  		})
    79  	}
    80  }
    81  
    82  // Establish that nested messages are tested.
    83  func TestMessageRuleNested(t *testing.T) {
    84  	// Create a file descriptor with a message and nested message in it.
    85  	fd, err := builder.NewFile("test.proto").AddMessage(
    86  		builder.NewMessage("Book").AddNestedMessage(builder.NewMessage("Author")),
    87  	).Build()
    88  	if err != nil {
    89  		t.Fatalf("Failed to build file descriptor.")
    90  	}
    91  
    92  	// Iterate over the tests and run them.
    93  	for _, test := range makeLintRuleTests(fd.GetMessageTypes()[0].GetNestedMessageTypes()[0]) {
    94  		t.Run(test.testName, func(t *testing.T) {
    95  			// Create the message rule.
    96  			rule := &MessageRule{
    97  				Name: RuleName("test"),
    98  				OnlyIf: func(m *desc.MessageDescriptor) bool {
    99  					return m.GetName() == "Author"
   100  				},
   101  				LintMessage: func(m *desc.MessageDescriptor) []Problem {
   102  					return test.problems
   103  				},
   104  			}
   105  
   106  			// Run the rule and assert that we got what we expect.
   107  			test.runRule(rule, fd, t)
   108  		})
   109  	}
   110  }
   111  
   112  func TestFieldRule(t *testing.T) {
   113  	// Create a file descriptor with one message and two fields in that message.
   114  	fd, err := builder.NewFile("test.proto").AddMessage(
   115  		builder.NewMessage("Book").AddField(
   116  			builder.NewField("title", builder.FieldTypeString()),
   117  		).AddField(
   118  			builder.NewField("edition_count", builder.FieldTypeInt32()),
   119  		),
   120  	).Build()
   121  	if err != nil {
   122  		t.Fatalf("Failed to build file descriptor.")
   123  	}
   124  
   125  	// Iterate over the tests and run them.
   126  	for _, test := range makeLintRuleTests(fd.GetMessageTypes()[0].GetFields()[1]) {
   127  		t.Run(test.testName, func(t *testing.T) {
   128  			// Create the field rule.
   129  			rule := &FieldRule{
   130  				Name: RuleName("test"),
   131  				OnlyIf: func(f *desc.FieldDescriptor) bool {
   132  					return f.GetName() == "edition_count"
   133  				},
   134  				LintField: func(f *desc.FieldDescriptor) []Problem {
   135  					return test.problems
   136  				},
   137  			}
   138  
   139  			// Run the rule and assert that we got what we expect.
   140  			test.runRule(rule, fd, t)
   141  		})
   142  	}
   143  }
   144  
   145  func TestServiceRule(t *testing.T) {
   146  	// Create a file descriptor with a service.
   147  	fd, err := builder.NewFile("test.proto").AddService(
   148  		builder.NewService("Library"),
   149  	).Build()
   150  	if err != nil {
   151  		t.Fatalf("Failed to build a file descriptor: %q", err)
   152  	}
   153  
   154  	// Iterate over the tests and run them.
   155  	for _, test := range makeLintRuleTests(fd.GetServices()[0]) {
   156  		t.Run(test.testName, func(t *testing.T) {
   157  			// Create the service rule.
   158  			rule := &ServiceRule{
   159  				Name: RuleName("test"),
   160  				LintService: func(s *desc.ServiceDescriptor) []Problem {
   161  					return test.problems
   162  				},
   163  			}
   164  
   165  			// Run the rule and assert that we got what we expect.
   166  			test.runRule(rule, fd, t)
   167  		})
   168  	}
   169  }
   170  
   171  func TestMethodRule(t *testing.T) {
   172  	// Create a file descriptor with a service.
   173  	book := builder.RpcTypeMessage(builder.NewMessage("Book"), false)
   174  	fd, err := builder.NewFile("test.proto").AddService(
   175  		builder.NewService("Library").AddMethod(
   176  			builder.NewMethod(
   177  				"GetBook",
   178  				builder.RpcTypeMessage(builder.NewMessage("GetBookRequest"), false),
   179  				book,
   180  			),
   181  		).AddMethod(
   182  			builder.NewMethod(
   183  				"CreateBook",
   184  				builder.RpcTypeMessage(builder.NewMessage("CreateBookRequest"), false),
   185  				book,
   186  			),
   187  		),
   188  	).Build()
   189  	if err != nil {
   190  		t.Fatalf("Failed to build a file descriptor: %q", err)
   191  	}
   192  
   193  	// Iterate over the tests and run them.
   194  	for _, test := range makeLintRuleTests(fd.GetServices()[0].GetMethods()[1]) {
   195  		t.Run(test.testName, func(t *testing.T) {
   196  			// Create the method rule.
   197  			rule := &MethodRule{
   198  				Name: RuleName("test"),
   199  				OnlyIf: func(m *desc.MethodDescriptor) bool {
   200  					return m.GetName() == "CreateBook"
   201  				},
   202  				LintMethod: func(m *desc.MethodDescriptor) []Problem {
   203  					return test.problems
   204  				},
   205  			}
   206  
   207  			// Run the rule and assert that we got what we expect.
   208  			test.runRule(rule, fd, t)
   209  		})
   210  	}
   211  }
   212  
   213  func TestEnumRule(t *testing.T) {
   214  	// Create a file descriptor with top-level enums.
   215  	fd, err := builder.NewFile("test.proto").AddEnum(
   216  		builder.NewEnum("Format").AddValue(builder.NewEnumValue("PDF")),
   217  	).AddEnum(
   218  		builder.NewEnum("Edition").AddValue(builder.NewEnumValue("PUBLISHER_ONLY")),
   219  	).Build()
   220  	if err != nil {
   221  		t.Fatalf("Error building test proto:%s ", err)
   222  	}
   223  
   224  	for _, test := range makeLintRuleTests(fd.GetEnumTypes()[1]) {
   225  		t.Run(test.testName, func(t *testing.T) {
   226  			// Create the enum rule.
   227  			rule := &EnumRule{
   228  				Name: RuleName("test"),
   229  				OnlyIf: func(e *desc.EnumDescriptor) bool {
   230  					return e.GetName() == "Edition"
   231  				},
   232  				LintEnum: func(e *desc.EnumDescriptor) []Problem {
   233  					return test.problems
   234  				},
   235  			}
   236  
   237  			// Run the rule and assert that we got what we expect.
   238  			test.runRule(rule, fd, t)
   239  		})
   240  	}
   241  }
   242  
   243  func TestEnumValueRule(t *testing.T) {
   244  	// Create a file descriptor with a top-level enum with values.
   245  	fd, err := builder.NewFile("test.proto").AddEnum(
   246  		builder.NewEnum("Format").AddValue(builder.NewEnumValue("YAML")).AddValue(builder.NewEnumValue("JSON")),
   247  	).Build()
   248  	if err != nil {
   249  		t.Fatalf("Error building test proto:%s ", err)
   250  	}
   251  
   252  	for _, test := range makeLintRuleTests(fd.GetEnumTypes()[0].GetValues()[1]) {
   253  		t.Run(test.testName, func(t *testing.T) {
   254  			// Create the enum value rule.
   255  			rule := &EnumValueRule{
   256  				Name: RuleName("test"),
   257  				OnlyIf: func(e *desc.EnumValueDescriptor) bool {
   258  					return e.GetName() == "JSON"
   259  				},
   260  				LintEnumValue: func(e *desc.EnumValueDescriptor) []Problem {
   261  					return test.problems
   262  				},
   263  			}
   264  
   265  			// Run the rule and assert that we got what we expect.
   266  			test.runRule(rule, fd, t)
   267  		})
   268  	}
   269  }
   270  
   271  func TestEnumRuleNested(t *testing.T) {
   272  	// Create a file descriptor with top-level enums.
   273  	fd, err := builder.NewFile("test.proto").AddMessage(
   274  		builder.NewMessage("Book").AddNestedEnum(
   275  			builder.NewEnum("Format").AddValue(builder.NewEnumValue("PDF")),
   276  		).AddNestedEnum(
   277  			builder.NewEnum("Edition").AddValue(builder.NewEnumValue("PUBLISHER_ONLY")),
   278  		),
   279  	).Build()
   280  	if err != nil {
   281  		t.Fatalf("Error building test proto:%s ", err)
   282  	}
   283  
   284  	for _, test := range makeLintRuleTests(fd.GetMessageTypes()[0].GetNestedEnumTypes()[1]) {
   285  		t.Run(test.testName, func(t *testing.T) {
   286  			// Create the enum rule.
   287  			rule := &EnumRule{
   288  				Name: RuleName("test"),
   289  				OnlyIf: func(e *desc.EnumDescriptor) bool {
   290  					return e.GetName() == "Edition"
   291  				},
   292  				LintEnum: func(e *desc.EnumDescriptor) []Problem {
   293  					return test.problems
   294  				},
   295  			}
   296  
   297  			// Run the rule and assert that we got what we expect.
   298  			test.runRule(rule, fd, t)
   299  		})
   300  	}
   301  }
   302  
   303  func TestDescriptorRule(t *testing.T) {
   304  	// Create a file with one of everything in it.
   305  	book := builder.NewMessage("Book").AddNestedEnum(
   306  		builder.NewEnum("Format").AddValue(
   307  			builder.NewEnumValue("FORMAT_UNSPECIFIED"),
   308  		).AddValue(builder.NewEnumValue("PAPERBACK")),
   309  	).AddField(builder.NewField("name", builder.FieldTypeString())).AddNestedMessage(
   310  		builder.NewMessage("Author"),
   311  	)
   312  	fd, err := builder.NewFile("library.proto").AddMessage(book).AddService(
   313  		builder.NewService("Library").AddMethod(
   314  			builder.NewMethod(
   315  				"ConjureBook",
   316  				builder.RpcTypeMessage(book, false),
   317  				builder.RpcTypeMessage(book, false),
   318  			),
   319  		),
   320  	).AddEnum(builder.NewEnum("State").AddValue(builder.NewEnumValue("AVAILABLE"))).Build()
   321  	if err != nil {
   322  		t.Fatalf("%v", err)
   323  	}
   324  
   325  	// Create a rule that lets us verify that each descriptor was visited.
   326  	visited := make(map[string]desc.Descriptor)
   327  	rule := &DescriptorRule{
   328  		Name: RuleName("test"),
   329  		OnlyIf: func(d desc.Descriptor) bool {
   330  			return d.GetName() != "FORMAT_UNSPECIFIED"
   331  		},
   332  		LintDescriptor: func(d desc.Descriptor) []Problem {
   333  			visited[d.GetName()] = d
   334  			return nil
   335  		},
   336  	}
   337  
   338  	// Run the rule.
   339  	rule.Lint(fd)
   340  
   341  	// Verify that each descriptor was visited.
   342  	// We do not care what order they were visited in.
   343  	wantDescriptors := []string{
   344  		"Author", "Book", "ConjureBook", "Format", "PAPERBACK",
   345  		"name", "Library", "State", "AVAILABLE",
   346  	}
   347  	if got, want := rule.GetName(), "test"; string(got) != want {
   348  		t.Errorf("Got name %q, wanted %q", got, want)
   349  	}
   350  	if got, want := len(visited), len(wantDescriptors); got != want {
   351  		t.Errorf("Got %d descriptors, wanted %d.", got, want)
   352  	}
   353  	for _, name := range wantDescriptors {
   354  		if _, ok := visited[name]; !ok {
   355  			t.Errorf("Missing descriptor %q.", name)
   356  		}
   357  	}
   358  }
   359  
   360  type lintRuleTest struct {
   361  	testName string
   362  	problems []Problem
   363  }
   364  
   365  // runRule runs a rule within a test environment.
   366  func (test *lintRuleTest) runRule(rule ProtoRule, fd *desc.FileDescriptor, t *testing.T) {
   367  	// Establish that the metadata methods work.
   368  	if got, want := string(rule.GetName()), string(RuleName("test")); got != want {
   369  		t.Errorf("Got %q for GetName(), expected %q", got, want)
   370  	}
   371  
   372  	// Run the rule's lint function on the file descriptor
   373  	// and assert that we got what we expect.
   374  	if got, want := rule.Lint(fd), test.problems; !reflect.DeepEqual(got, want) {
   375  		t.Errorf("Got %v problems; expected %v.", got, want)
   376  	}
   377  }
   378  
   379  // makeLintRuleTests generates boilerplate tests that are consistent for
   380  // each type of rule.
   381  func makeLintRuleTests(d desc.Descriptor) []lintRuleTest {
   382  	return []lintRuleTest{
   383  		{"NoProblems", []Problem{}},
   384  		{"OneProblem", []Problem{{
   385  			Message:    "There was a problem.",
   386  			Descriptor: d,
   387  		}}},
   388  		{"TwoProblems", []Problem{
   389  			{
   390  				Message:    "This was the first problem.",
   391  				Descriptor: d,
   392  			},
   393  			{
   394  				Message:    "This was the second problem.",
   395  				Descriptor: d,
   396  			},
   397  		}},
   398  	}
   399  }