github.com/googleapis/api-linter@v1.65.2/cmd/api-linter/integration_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 main
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  )
    27  
    28  // Each case must be positive when the rule in test
    29  // is enabled. It must also contain a "disable-me-here"
    30  // comment at the place where you want the rule to be
    31  // disabled.
    32  var testCases = []struct {
    33  	testName, rule, proto string
    34  }{
    35  	{
    36  		testName: "GetRequestMessage",
    37  		rule:     "core::0131::request-message-name",
    38  		proto: `
    39  		syntax = "proto3";
    40  
    41  		service Library {
    42  			// disable-me-here
    43  			rpc GetBook(Book) returns (Book);
    44  		}
    45  
    46  		message Book {}
    47  		`,
    48  	},
    49  	{
    50  		testName: "PackageVersion",
    51  		rule:     "core::0215::versioned-packages",
    52  		proto: `
    53  		syntax = "proto3";
    54  
    55  		// disable-me-here
    56  		package google.test;
    57  
    58  		message Test {}
    59  		`,
    60  	},
    61  	{
    62  		testName: "FieldNames",
    63  		rule:     "core::0140::lower-snake",
    64  		proto: `
    65  				syntax = "proto3";
    66  				import "dummy.proto";
    67  				message Test {
    68  					// disable-me-here
    69  					string badName = 1;
    70  					dummy.Dummy dummy = 2;
    71  				}
    72  			`,
    73  	},
    74  }
    75  
    76  func TestRules_EnabledByDefault(t *testing.T) {
    77  	for _, test := range testCases {
    78  		t.Run(test.testName, func(t *testing.T) {
    79  			proto := test.proto
    80  			result := runLinter(t, proto, "")
    81  			if !strings.Contains(result, test.rule) {
    82  				t.Errorf("rule %q should be enabled by default", test.rule)
    83  			}
    84  		})
    85  	}
    86  }
    87  
    88  func TestRules_DisabledByFileComments(t *testing.T) {
    89  	for _, test := range testCases {
    90  		t.Run(test.testName, func(t *testing.T) {
    91  			disableInFile := fmt.Sprintf("// (-- api-linter: %s=disabled --)", test.rule)
    92  			proto := disableInFile + "\n" + test.proto
    93  			result := runLinter(t, proto, "")
    94  			if strings.Contains(result, test.rule) {
    95  				t.Errorf("rule %q should be disabled by file comments", test.rule)
    96  			}
    97  		})
    98  	}
    99  }
   100  
   101  func TestRules_DisabledByInlineComments(t *testing.T) {
   102  	testConfigurations := []struct {
   103  		suffix       string
   104  		appendArgs   []string
   105  		wantDisabled bool
   106  	}{
   107  		{
   108  			suffix:       "WithIgnoreCommentDisablesTrue",
   109  			appendArgs:   []string{"--ignore-comment-disables=true"},
   110  			wantDisabled: false,
   111  		},
   112  		{
   113  			suffix:       "WithIgnoreCommentDisablesFalse",
   114  			appendArgs:   []string{"--ignore-comment-disables=false"},
   115  			wantDisabled: true,
   116  		},
   117  		{
   118  			suffix:       "WithNoFlags",
   119  			appendArgs:   []string{},
   120  			wantDisabled: true,
   121  		},
   122  	}
   123  
   124  	for _, test := range testCases {
   125  		for _, testConfig := range testConfigurations {
   126  			t.Run(test.testName+testConfig.suffix, func(t *testing.T) {
   127  				disableInline := fmt.Sprintf("(-- api-linter: %s=disabled --)", test.rule)
   128  				proto := strings.Replace(test.proto, "disable-me-here", disableInline, -1)
   129  				_, result := runLinterWithFailureStatus(t, proto, "", testConfig.appendArgs)
   130  				isDisabled := !strings.Contains(result, test.rule)
   131  				if isDisabled != testConfig.wantDisabled {
   132  					t.Errorf("want %q disabled by in-line comments to be %v with flags %v, got %v", test.rule, testConfig.wantDisabled, testConfig.appendArgs, isDisabled)
   133  				}
   134  			})
   135  		}
   136  	}
   137  }
   138  
   139  func TestRules_DisabledByConfig(t *testing.T) {
   140  	config := `
   141  	[
   142  		{
   143  			"disabled_rules": ["replace-me-here"]
   144  		}
   145  	]
   146  	`
   147  
   148  	for _, test := range testCases {
   149  		t.Run(test.testName, func(t *testing.T) {
   150  			c := strings.Replace(config, "replace-me-here", test.rule, -1)
   151  			result := runLinter(t, test.proto, c)
   152  			if strings.Contains(result, test.rule) {
   153  				t.Errorf("rule %q should be disabled by the user config: %q", test.rule, c)
   154  			}
   155  		})
   156  	}
   157  }
   158  
   159  func TestBuildErrors(t *testing.T) {
   160  	expected := []string{
   161  		"internal/testdata/build_errors.proto:8:1:",
   162  		"internal/testdata/build_errors.proto:13:1:",
   163  	}
   164  	err := runCLI([]string{"internal/testdata/build_errors.proto"})
   165  	if err == nil {
   166  		t.Fatal("expected build error for build_errors.proto")
   167  	}
   168  	actual := err.Error()
   169  	actualLines := strings.Split(strings.TrimSpace(actual), "\n")
   170  	for idx, line := range actualLines {
   171  		if idx := strings.IndexByte(line, ' '); idx > -1 {
   172  			line = line[:idx]
   173  		}
   174  		actualLines[idx] = line
   175  	}
   176  	if diff := cmp.Diff(expected, actualLines); diff != "" {
   177  		t.Fatalf("unexpected errors: diff (-want +got):\n%s", diff)
   178  	}
   179  }
   180  
   181  func TestExitStatusForLintFailure(t *testing.T) {
   182  	type testCase struct{ testName, rule, proto string }
   183  	failCase := testCase{
   184  		testName: "GetRequestMessage",
   185  		rule:     "core::0131::request-message-name",
   186  		proto: `
   187  		syntax = "proto3";
   188  
   189  		service Library {
   190  			// disable-me-here
   191  			rpc GetBook(Book) returns (Book);
   192  		}
   193  
   194  		message Book {}
   195  		`,
   196  	}
   197  	// checks lint failure = true when lint problems found
   198  	t.Run(failCase.testName+"ReturnsFailure", func(t *testing.T) {
   199  		lintFailureStatus, result := runLinterWithFailureStatus(t, failCase.proto, "", []string{"--set-exit-status"})
   200  		if lintFailureStatus == false {
   201  			t.Log(result)
   202  			t.Fatalf("Expected: %v Actual: %v", true, lintFailureStatus)
   203  		}
   204  	})
   205  
   206  	// checks lint failure = false when no problems found
   207  	t.Run(failCase.testName+"ReturnsNoFailure", func(t *testing.T) {
   208  		disableAll := `[ { "disabled_rules": [ "all" ] } ]`
   209  		lintFailureStatus, result := runLinterWithFailureStatus(t, failCase.proto, disableAll, []string{"--set-exit-status"})
   210  		if lintFailureStatus {
   211  			t.Log(result)
   212  		}
   213  		if lintFailureStatus == true {
   214  			t.Fatalf("Expected: %v Actual: %v", false, lintFailureStatus)
   215  		}
   216  	})
   217  
   218  	// checks lint failure = false when lint problems found but --set-exit-status not set
   219  	for _, test := range testCases {
   220  		t.Run(test.testName, func(t *testing.T) {
   221  			proto := test.proto
   222  			lintFailureStatus, result := runLinterWithFailureStatus(t, proto, "", []string{})
   223  			expected := result == ""
   224  			if lintFailureStatus != expected {
   225  				t.Fatalf("Expected: %v Actual: %v", expected, lintFailureStatus)
   226  			}
   227  		})
   228  	}
   229  }
   230  
   231  func runLinter(t *testing.T, protoContent, configContent string) string {
   232  	_, result := runLinterWithFailureStatus(t, protoContent, configContent, []string{})
   233  	return result
   234  }
   235  
   236  func runLinterWithFailureStatus(t *testing.T, protoContent, configContent string, appendArgs []string) (bool, string) {
   237  	tempDir, err := os.MkdirTemp("", "test")
   238  	if err != nil {
   239  		t.Fatal(err)
   240  	}
   241  	defer os.RemoveAll(tempDir)
   242  
   243  	// Prepare command line flags.
   244  	args := []string{}
   245  	// Add a flag for the linter config file if the provided
   246  	// config content is not empty.
   247  	if configContent != "" {
   248  		configFileName := "test_config.json"
   249  		configFilePath := filepath.Join(tempDir, configFileName)
   250  		if err := writeFile(configFilePath, configContent); err != nil {
   251  			t.Fatal(err)
   252  		}
   253  		args = append(args, fmt.Sprintf("--config=%s", configFilePath))
   254  	}
   255  	// Add a flag for the output path.
   256  	outPath := filepath.Join(tempDir, "test.out")
   257  	args = append(args, fmt.Sprintf("-o=%s", outPath))
   258  	// Add the temp dir to the proto paths.
   259  	args = append(args, fmt.Sprintf("-I=%s", tempDir))
   260  	// Add a flag for the file descriptor set.
   261  	args = append(args, "--descriptor-set-in=internal/testdata/dummy.protoset")
   262  	// Write the proto file.
   263  	protoFileName := "test.proto"
   264  	protoFilePath := filepath.Join(tempDir, protoFileName)
   265  	if err := writeFile(protoFilePath, protoContent); err != nil {
   266  		t.Fatal(err)
   267  	}
   268  	args = append(args, protoFileName)
   269  	args = append(args, appendArgs...)
   270  
   271  	lintErr := runCLI(args)
   272  	if lintErr != nil && !errors.Is(lintErr, ExitForLintFailure) {
   273  		t.Fatal(lintErr)
   274  	}
   275  
   276  	out, err := os.ReadFile(outPath)
   277  	if err != nil {
   278  		t.Fatal(err)
   279  	}
   280  	return errors.Is(lintErr, ExitForLintFailure), string(out)
   281  }
   282  
   283  func writeFile(path, content string) error {
   284  	if path == "" {
   285  		return nil
   286  	}
   287  	err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
   288  	if err != nil {
   289  		return err
   290  	}
   291  	return os.WriteFile(path, []byte(content), 0o644)
   292  }