github.com/jhump/protocompile@v0.0.0-20221021153901-4f6f732835e8/parser/validate_test.go (about)

     1  package parser
     2  
     3  import (
     4  	"strings"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  
     9  	"github.com/jhump/protocompile/reporter"
    10  )
    11  
    12  func TestBasicValidation(t *testing.T) {
    13  	testCases := []struct {
    14  		contents string
    15  		succeeds bool
    16  		errMsg   string
    17  	}{
    18  		{
    19  			contents: `message Foo { optional double bar = 1 [default = -18446744073709551615]; }`,
    20  			succeeds: true,
    21  		},
    22  		{
    23  			// with byte order marker
    24  			contents: string([]byte{0xEF, 0xBB, 0xBF}) + `message Foo { optional double bar = 1 [default = -18446744073709551615]; }`,
    25  			succeeds: true,
    26  		},
    27  		{
    28  			contents: `message Foo { optional double bar = 1 [default = 18446744073709551616]; }`,
    29  			succeeds: true,
    30  		},
    31  		{
    32  			contents: `message Foo { extensions 100 to max; option message_set_wire_format = true; } message Bar { } extend Foo { optional Bar bar = 536870912; }`,
    33  			succeeds: true,
    34  		},
    35  		{
    36  			contents: `message Foo { option message_set_wire_format = true; extensions 1 to 100; }`,
    37  			succeeds: true,
    38  		},
    39  		{
    40  			contents: `message Foo { optional double bar = 536870912; option message_set_wire_format = true; }`,
    41  			errMsg:   "test.proto:1:15: messages with message-set wire format cannot contain non-extension fields",
    42  		},
    43  		{
    44  			contents: `message Foo { option message_set_wire_format = true; }`,
    45  			errMsg:   "test.proto:1:15: messages with message-set wire format must contain at least one extension range",
    46  		},
    47  		{
    48  			contents: `message Foo { oneof bar { group Baz = 1 [deprecated=true] { optional int32 abc = 1; } } }`,
    49  			succeeds: true,
    50  		},
    51  		{
    52  			contents: `syntax = "proto1";`,
    53  			errMsg:   `test.proto:1:10: syntax value must be "proto2" or "proto3"`,
    54  		},
    55  		{
    56  			contents: `message Foo { optional string s = 5000000000; }`,
    57  			errMsg:   `test.proto:1:35: tag number 5000000000 is higher than max allowed tag number (536870911)`,
    58  		},
    59  		{
    60  			contents: `message Foo { optional string s = 19500; }`,
    61  			errMsg:   `test.proto:1:35: tag number 19500 is in disallowed reserved range 19000-19999`,
    62  		},
    63  		{
    64  			contents: `enum Foo { V = 5000000000; }`,
    65  			errMsg:   `test.proto:1:16: value 5000000000 is out of range: should be between -2147483648 and 2147483647`,
    66  		},
    67  		{
    68  			contents: `enum Foo { V = -5000000000; }`,
    69  			errMsg:   `test.proto:1:16: value -5000000000 is out of range: should be between -2147483648 and 2147483647`,
    70  		},
    71  		{
    72  			contents: `enum Foo { V = 0; reserved 5000000000; }`,
    73  			errMsg:   `test.proto:1:28: range start 5000000000 is out of range: should be between -2147483648 and 2147483647`,
    74  		},
    75  		{
    76  			contents: `enum Foo { V = 0; reserved -5000000000; }`,
    77  			errMsg:   `test.proto:1:28: range start -5000000000 is out of range: should be between -2147483648 and 2147483647`,
    78  		},
    79  		{
    80  			contents: `enum Foo { V = 0; reserved 5000000000 to 5000000001; }`,
    81  			errMsg:   `test.proto:1:28: range start 5000000000 is out of range: should be between -2147483648 and 2147483647`,
    82  		},
    83  		{
    84  			contents: `enum Foo { V = 0; reserved 5 to 5000000000; }`,
    85  			errMsg:   `test.proto:1:33: range end 5000000000 is out of range: should be between -2147483648 and 2147483647`,
    86  		},
    87  		{
    88  			contents: `enum Foo { V = 0; reserved -5000000000 to -5; }`,
    89  			errMsg:   `test.proto:1:28: range start -5000000000 is out of range: should be between -2147483648 and 2147483647`,
    90  		},
    91  		{
    92  			contents: `enum Foo { V = 0; reserved -5000000001 to -5000000000; }`,
    93  			errMsg:   `test.proto:1:28: range start -5000000001 is out of range: should be between -2147483648 and 2147483647`,
    94  		},
    95  		{
    96  			contents: `enum Foo { V = 0; reserved -5000000000 to 5; }`,
    97  			errMsg:   `test.proto:1:28: range start -5000000000 is out of range: should be between -2147483648 and 2147483647`,
    98  		},
    99  		{
   100  			contents: `enum Foo { V = 0; reserved -5 to 5000000000; }`,
   101  			errMsg:   `test.proto:1:34: range end 5000000000 is out of range: should be between -2147483648 and 2147483647`,
   102  		},
   103  		{
   104  			contents: `enum Foo { }`,
   105  			errMsg:   `test.proto:1:1: enum Foo: enums must define at least one value`,
   106  		},
   107  		{
   108  			contents: `message Foo { oneof Bar { } }`,
   109  			errMsg:   `test.proto:1:15: oneof must contain at least one field`,
   110  		},
   111  		{
   112  			contents: `message Foo { extensions 1 to max; } extend Foo { }`,
   113  			errMsg:   `test.proto:1:38: extend sections must define at least one extension`,
   114  		},
   115  		{
   116  			contents: `message Foo { option map_entry = true; }`,
   117  			errMsg:   `test.proto:1:34: message Foo: map_entry option should not be set explicitly; use map type instead`,
   118  		},
   119  		{
   120  			contents: `message Foo { option map_entry = false; }`,
   121  			succeeds: true, // okay if explicit setting is false
   122  		},
   123  		{
   124  			contents: `syntax = "proto2"; message Foo { string s = 1; }`,
   125  			errMsg:   `test.proto:1:41: field Foo.s: field has no label; proto2 requires explicit 'optional' label`,
   126  		},
   127  		{
   128  			contents: `message Foo { string s = 1; }`, // syntax defaults to proto2
   129  			errMsg:   `test.proto:1:22: field Foo.s: field has no label; proto2 requires explicit 'optional' label`,
   130  		},
   131  		{
   132  			contents: `syntax = "proto3"; message Foo { optional string s = 1; }`,
   133  			succeeds: true, // proto3_optional
   134  		},
   135  		{
   136  			contents: `syntax = "proto3"; import "google/protobuf/descriptor.proto"; extend google.protobuf.MessageOptions { optional string s = 50000; }`,
   137  			succeeds: true, // proto3_optional for extensions
   138  		},
   139  		{
   140  			contents: `syntax = "proto3"; message Foo { required string s = 1; }`,
   141  			errMsg:   `test.proto:1:34: field Foo.s: label 'required' is not allowed in proto3`,
   142  		},
   143  		{
   144  			contents: `message Foo { extensions 1 to max; } extend Foo { required string sss = 100; }`,
   145  			errMsg:   `test.proto:1:51: field sss: extension fields cannot be 'required'`,
   146  		},
   147  		{
   148  			contents: `syntax = "proto3"; message Foo { optional group Grp = 1 { } }`,
   149  			errMsg:   `test.proto:1:43: field Foo.grp: groups are not allowed in proto3`,
   150  		},
   151  		{
   152  			contents: `syntax = "proto3"; message Foo { extensions 1 to max; }`,
   153  			errMsg:   `test.proto:1:45: message Foo: extension ranges are not allowed in proto3`,
   154  		},
   155  		{
   156  			contents: `syntax = "proto3"; message Foo { string s = 1 [default = "abcdef"]; }`,
   157  			errMsg:   `test.proto:1:48: field Foo.s: default values are not allowed in proto3`,
   158  		},
   159  		{
   160  			contents: `enum Foo { V1 = 1; V2 = 1; }`,
   161  			errMsg:   `test.proto:1:25: enum Foo: values V1 and V2 both have the same numeric value 1; use allow_alias option if intentional`,
   162  		},
   163  		{
   164  			contents: `enum Foo { option allow_alias = true; V1 = 1; V2 = 1; }`,
   165  			succeeds: true,
   166  		},
   167  		{
   168  			contents: `enum Foo { option allow_alias = false; V1 = 1; V2 = 2; }`,
   169  			succeeds: true,
   170  		},
   171  		{
   172  			contents: `enum Foo { option allow_alias = true; V1 = 1; V2 = 2; }`,
   173  			errMsg:   `test.proto:1:33: enum Foo: allow_alias is true but no values are aliases`,
   174  		},
   175  		{
   176  			contents: `syntax = "proto3"; enum Foo { V1 = 0; reserved 1 to 20; reserved "V2"; }`,
   177  			succeeds: true,
   178  		},
   179  		{
   180  			contents: `enum Foo { V1 = 1; reserved 1 to 20; reserved "V2"; }`,
   181  			errMsg:   `test.proto:1:17: enum Foo: value V1 is using number 1 which is in reserved range 1 to 20`,
   182  		},
   183  		{
   184  			contents: `enum Foo { V1 = 20; reserved 1 to 20; reserved "V2"; }`,
   185  			errMsg:   `test.proto:1:17: enum Foo: value V1 is using number 20 which is in reserved range 1 to 20`,
   186  		},
   187  		{
   188  			contents: `enum Foo { V2 = 0; reserved 1 to 20; reserved "V2"; }`,
   189  			errMsg:   `test.proto:1:12: enum Foo: value V2 is using a reserved name`,
   190  		},
   191  		{
   192  			contents: `enum Foo { V0 = 0; reserved 1 to 20; reserved 21 to 40; reserved "V2"; }`,
   193  			succeeds: true,
   194  		},
   195  		{
   196  			contents: `enum Foo { V0 = 0; reserved 1 to 20; reserved 20 to 40; reserved "V2"; }`,
   197  			errMsg:   `test.proto:1:47: enum Foo: reserved ranges overlap: 1 to 20 and 20 to 40`,
   198  		},
   199  		{
   200  			contents: `syntax = "proto3"; enum Foo { FIRST = 1; }`,
   201  			errMsg:   `test.proto:1:39: enum Foo: proto3 requires that first value in enum have numeric value of 0`,
   202  		},
   203  		{
   204  			contents: `syntax = "proto3"; message Foo { string s = 1; int32 i = 1; }`,
   205  			errMsg:   `test.proto:1:58: message Foo: fields s and i both have the same tag 1`,
   206  		},
   207  		{
   208  			contents: `message Foo { reserved 1 to 10, 10 to 12; }`,
   209  			errMsg:   `test.proto:1:33: message Foo: reserved ranges overlap: 1 to 10 and 10 to 12`,
   210  		},
   211  		{
   212  			contents: `message Foo { extensions 1 to 10, 10 to 12; }`,
   213  			errMsg:   `test.proto:1:35: message Foo: extension ranges overlap: 1 to 10 and 10 to 12`,
   214  		},
   215  		{
   216  			contents: `message Foo { reserved 1 to 10; extensions 10 to 12; }`,
   217  			errMsg:   `test.proto:1:44: message Foo: extension range 10 to 12 overlaps reserved range 1 to 10`,
   218  		},
   219  		{
   220  			contents: `message Foo { reserved 1, 2 to 10, 11 to 20; extensions 21 to 22; }`,
   221  			succeeds: true,
   222  		},
   223  		{
   224  			contents: `message Foo { reserved 10 to 1; }`,
   225  			errMsg:   `test.proto:1:24: range, 10 to 1, is invalid: start must be <= end`,
   226  		},
   227  		{
   228  			contents: `message Foo { extensions 10 to 1; }`,
   229  			errMsg:   `test.proto:1:26: range, 10 to 1, is invalid: start must be <= end`,
   230  		},
   231  		{
   232  			contents: `message Foo { reserved 1 to 5000000000; }`,
   233  			errMsg:   `test.proto:1:29: range end 5000000000 is out of range: should be between 1 and 536870911`,
   234  		},
   235  		{
   236  			contents: `message Foo { reserved 0 to 10; }`,
   237  			errMsg:   `test.proto:1:24: range start 0 is out of range: should be between 1 and 536870911`,
   238  		},
   239  		{
   240  			contents: `message Foo { extensions 3000000000; }`,
   241  			errMsg:   `test.proto:1:26: range start 3000000000 is out of range: should be between 1 and 536870911`,
   242  		},
   243  		{
   244  			contents: `message Foo { extensions 3000000000 to 3000000001; }`,
   245  			errMsg:   `test.proto:1:26: range start 3000000000 is out of range: should be between 1 and 536870911`,
   246  		},
   247  		{
   248  			contents: `message Foo { extensions 0 to 10; }`,
   249  			errMsg:   `test.proto:1:26: range start 0 is out of range: should be between 1 and 536870911`,
   250  		},
   251  		{
   252  			contents: `message Foo { extensions 100 to 3000000000; }`,
   253  			errMsg:   `test.proto:1:33: range end 3000000000 is out of range: should be between 1 and 536870911`,
   254  		},
   255  		{
   256  			contents: `message Foo { reserved "foo", "foo"; }`,
   257  			errMsg:   `test.proto:1:31: name "foo" is reserved multiple times`,
   258  		},
   259  		{
   260  			contents: `message Foo { reserved "foo"; reserved "foo"; }`,
   261  			errMsg:   `test.proto:1:40: name "foo" is reserved multiple times`,
   262  		},
   263  		{
   264  			contents: `message Foo { reserved "foo"; optional string foo = 1; }`,
   265  			errMsg:   `test.proto:1:47: message Foo: field foo is using a reserved name`,
   266  		},
   267  		{
   268  			contents: `message Foo { reserved 1 to 10; optional string foo = 1; }`,
   269  			errMsg:   `test.proto:1:55: message Foo: field foo is using tag 1 which is in reserved range 1 to 10`,
   270  		},
   271  		{
   272  			contents: `message Foo { extensions 1 to 10; optional string foo = 1; }`,
   273  			errMsg:   `test.proto:1:57: message Foo: field foo is using tag 1 which is in extension range 1 to 10`,
   274  		},
   275  		{
   276  			contents: `message Foo { optional group foo = 1 { } }`,
   277  			errMsg:   `test.proto:1:30: group foo should have a name that starts with a capital letter`,
   278  		},
   279  		{
   280  			contents: `message Foo { oneof foo { group bar = 1 { } } }`,
   281  			errMsg:   `test.proto:1:33: group bar should have a name that starts with a capital letter`,
   282  		},
   283  		{
   284  			contents: `enum Foo { option = 1; }`,
   285  			errMsg:   `test.proto:1:19: syntax error: unexpected '='`,
   286  		},
   287  		{
   288  			contents: `enum Foo { reserved = 1; }`,
   289  			errMsg:   `test.proto:1:21: syntax error: unexpected '=', expecting string literal or int literal or '-'`,
   290  		},
   291  		{
   292  			contents: `syntax = "proto3"; enum message { unset = 0; } message Foo { message bar = 1; }`,
   293  			errMsg:   `test.proto:1:74: syntax error: unexpected '=', expecting '{'`,
   294  		},
   295  		{
   296  			contents: `syntax = "proto3"; enum enum { unset = 0; } message Foo { enum bar = 1; }`,
   297  			errMsg:   `test.proto:1:68: syntax error: unexpected '=', expecting '{'`,
   298  		},
   299  		{
   300  			contents: `syntax = "proto3"; enum reserved { unset = 0; } message Foo { reserved bar = 1; }`,
   301  			errMsg:   `test.proto:1:72: syntax error: unexpected identifier, expecting string literal or int literal`,
   302  		},
   303  		{
   304  			contents: `syntax = "proto3"; enum extend { unset = 0; } message Foo { extend bar = 1; }`,
   305  			errMsg:   `test.proto:1:72: syntax error: unexpected '=', expecting '{'`,
   306  		},
   307  		{
   308  			contents: `syntax = "proto3"; enum oneof { unset = 0; } message Foo { oneof bar = 1; }`,
   309  			errMsg:   `test.proto:1:70: syntax error: unexpected '=', expecting '{'`,
   310  		},
   311  		{
   312  			contents: `syntax = "proto3"; enum optional { unset = 0; } message Foo { optional bar = 1; }`,
   313  			errMsg:   `test.proto:1:76: syntax error: unexpected '='`,
   314  		},
   315  		{
   316  			contents: `syntax = "proto3"; enum repeated { unset = 0; } message Foo { repeated bar = 1; }`,
   317  			errMsg:   `test.proto:1:76: syntax error: unexpected '='`,
   318  		},
   319  		{
   320  			contents: `syntax = "proto3"; enum required { unset = 0; } message Foo { required bar = 1; }`,
   321  			errMsg:   `test.proto:1:76: syntax error: unexpected '='`,
   322  		},
   323  		{
   324  			contents: `syntax = "proto3"; import "google/protobuf/descriptor.proto"; enum optional { unset = 0; } extend google.protobuf.MethodOptions { optional bar = 22222; }`,
   325  			errMsg:   `test.proto:1:144: syntax error: unexpected '='`,
   326  		},
   327  		{
   328  			contents: `syntax = "proto3"; import "google/protobuf/descriptor.proto"; enum repeated { unset = 0; } extend google.protobuf.MethodOptions { repeated bar = 22222; }`,
   329  			errMsg:   `test.proto:1:144: syntax error: unexpected '='`,
   330  		},
   331  		{
   332  			contents: `syntax = "proto3"; import "google/protobuf/descriptor.proto"; enum required { unset = 0; } extend google.protobuf.MethodOptions { required bar = 22222; }`,
   333  			errMsg:   `test.proto:1:144: syntax error: unexpected '='`,
   334  		},
   335  		{
   336  			contents: `syntax = "proto3"; enum optional { unset = 0; } message Foo { oneof bar { optional bar = 1; } }`,
   337  			errMsg:   `test.proto:1:75: syntax error: unexpected "optional"`,
   338  		},
   339  		{
   340  			contents: `syntax = "proto3"; enum repeated { unset = 0; } message Foo { oneof bar { repeated bar = 1; } }`,
   341  			errMsg:   `test.proto:1:75: syntax error: unexpected "repeated"`,
   342  		},
   343  		{
   344  			contents: `syntax = "proto3"; enum required { unset = 0; } message Foo { oneof bar { required bar = 1; } }`,
   345  			errMsg:   `test.proto:1:75: syntax error: unexpected "required"`,
   346  		},
   347  		{
   348  			contents: ``,
   349  			succeeds: true,
   350  		},
   351  		{
   352  			contents: `0`,
   353  			errMsg:   `test.proto:1:1: syntax error: unexpected int literal`,
   354  		},
   355  		{
   356  			contents: `foobar`,
   357  			errMsg:   `test.proto:1:1: syntax error: unexpected identifier`,
   358  		},
   359  		{
   360  			contents: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`,
   361  			errMsg:   `test.proto:1:1: syntax error: unexpected identifier`,
   362  		},
   363  		{
   364  			contents: `"abc"`,
   365  			errMsg:   `test.proto:1:1: syntax error: unexpected string literal`,
   366  		},
   367  		{
   368  			contents: `0.0.0.0.0`,
   369  			errMsg:   `test.proto:1:1: invalid syntax in float value: 0.0.0.0.0`,
   370  		},
   371  		{
   372  			contents: `0.0`,
   373  			errMsg:   `test.proto:1:1: syntax error: unexpected float literal`,
   374  		},
   375  		{
   376  			contents: `option (opt) = {m: [{key: "a",value: {}}]};`,
   377  			succeeds: true,
   378  		},
   379  		{
   380  			contents: `option (opt) = {m [{key: "a",value: {}}]};`,
   381  			succeeds: true,
   382  		},
   383  		{
   384  			contents: `option (opt) = {m: []};`,
   385  			succeeds: true,
   386  		},
   387  		{
   388  			contents: `option (opt) = {m []};`,
   389  			succeeds: true,
   390  		},
   391  	}
   392  
   393  	for i, tc := range testCases {
   394  		errs := reporter.NewHandler(nil)
   395  		if ast, err := Parse("test.proto", strings.NewReader(tc.contents), errs); err == nil {
   396  			_, _ = ResultFromAST(ast, true, errs)
   397  		}
   398  
   399  		err := errs.Error()
   400  		if tc.succeeds {
   401  			assert.Nil(t, err, "case #%d should succeed", i)
   402  		} else {
   403  			if assert.NotNil(t, err, "case #%d should fail", i) {
   404  				assert.Equal(t, tc.errMsg, err.Error(), "case #%d bad error message", i)
   405  			}
   406  		}
   407  	}
   408  }