github.com/google/osv-scalibr@v0.4.1/veles/secrets/common/flatjson/flatjson_test.go (about)

     1  // Copyright 2025 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  //      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 flatjson_test
    16  
    17  import (
    18  	"testing"
    19  
    20  	"github.com/google/go-cmp/cmp"
    21  	"github.com/google/osv-scalibr/veles/secrets/common/flatjson"
    22  )
    23  
    24  type testExtractorSubCase struct {
    25  	name  string
    26  	input string
    27  	want  map[string]string
    28  }
    29  
    30  func TestExtractor(t *testing.T) {
    31  	cases := []struct {
    32  		name     string
    33  		required []string
    34  		optional []string
    35  		subs     []testExtractorSubCase
    36  	}{
    37  		{
    38  			name:     "no keys",
    39  			required: []string{},
    40  			optional: []string{},
    41  			subs: []testExtractorSubCase{
    42  				{
    43  					name:  "empty",
    44  					input: "",
    45  					want:  map[string]string{},
    46  				},
    47  				{
    48  					name:  "non-empty",
    49  					input: `{"key1": "value1", "key2": "value2"}`,
    50  					want:  map[string]string{},
    51  				},
    52  			},
    53  		},
    54  		{
    55  			name:     "only required",
    56  			required: []string{"foo", "bar", "baz"},
    57  			optional: []string{},
    58  			subs: []testExtractorSubCase{
    59  				{
    60  					name:  "empty",
    61  					input: "",
    62  					want:  nil,
    63  				},
    64  				{
    65  					name:  "required key missing",
    66  					input: `{"foo": "hello", "bar": "world"}`,
    67  					want:  nil,
    68  				},
    69  				{
    70  					name:  "all present",
    71  					input: `{"foo": "hello", "bar": "world", "baz": "12345"}`,
    72  					want: map[string]string{
    73  						"foo": "hello",
    74  						"bar": "world",
    75  						"baz": "12345",
    76  					},
    77  				},
    78  				{
    79  					name:  "extra keys ignored",
    80  					input: `{"foo": "hello", "bar": "world", "baz": "12345", "another": "ignored"}`,
    81  					want: map[string]string{
    82  						"foo": "hello",
    83  						"bar": "world",
    84  						"baz": "12345",
    85  					},
    86  				},
    87  			},
    88  		},
    89  		{
    90  			name:     "only optional",
    91  			required: []string{},
    92  			optional: []string{"foo", "bar", "baz"},
    93  			subs: []testExtractorSubCase{
    94  				{
    95  					name:  "empty",
    96  					input: "",
    97  					want:  map[string]string{},
    98  				},
    99  				{
   100  					name:  "subset present",
   101  					input: `{"foo": "hello", "bar": "world"}`,
   102  					want: map[string]string{
   103  						"foo": "hello",
   104  						"bar": "world",
   105  					},
   106  				},
   107  				{
   108  					name:  "all present",
   109  					input: `{"foo": "hello", "bar": "world", "baz": "12345"}`,
   110  					want: map[string]string{
   111  						"foo": "hello",
   112  						"bar": "world",
   113  						"baz": "12345",
   114  					},
   115  				},
   116  				{
   117  					name:  "extra keys ignored",
   118  					input: `{"foo": "hello", "bar": "world", "baz": "12345", "another": "ignored"}`,
   119  					want: map[string]string{
   120  						"foo": "hello",
   121  						"bar": "world",
   122  						"baz": "12345",
   123  					},
   124  				},
   125  			},
   126  		},
   127  		{
   128  			name:     "required and optional",
   129  			required: []string{"foo", "bar"},
   130  			optional: []string{"baz", "another"},
   131  			subs: []testExtractorSubCase{
   132  				{
   133  					name:  "empty",
   134  					input: "",
   135  					want:  nil,
   136  				},
   137  				{
   138  					name:  "missing required",
   139  					input: `{"foo": "hello", "baz": "meh"}`,
   140  					want:  nil,
   141  				},
   142  				{
   143  					name:  "only required",
   144  					input: `{"foo": "hello", "bar": "world"}`,
   145  					want: map[string]string{
   146  						"foo": "hello",
   147  						"bar": "world",
   148  					},
   149  				},
   150  				{
   151  					name:  "required and some optional",
   152  					input: `{"foo": "hello", "bar": "world", "baz": "12345"}`,
   153  					want: map[string]string{
   154  						"foo": "hello",
   155  						"bar": "world",
   156  						"baz": "12345",
   157  					},
   158  				},
   159  				{
   160  					name:  "required and optional",
   161  					input: `{"foo": "hello", "bar": "world", "baz": "12345", "another": "null"}`,
   162  					want: map[string]string{
   163  						"foo":     "hello",
   164  						"bar":     "world",
   165  						"baz":     "12345",
   166  						"another": "null",
   167  					},
   168  				},
   169  			},
   170  		},
   171  		{
   172  			name:     "only supports string values",
   173  			required: []string{"foo", "bar"},
   174  			optional: []string{"baz"},
   175  			subs: []testExtractorSubCase{
   176  				{
   177  					name:  "required is int",
   178  					input: `{"foo": "hello", "bar": 12345, "baz": "nooo"}`,
   179  					want:  nil,
   180  				},
   181  				{
   182  					name:  "optional is int",
   183  					input: `{"foo": "hello", "bar": "world", "baz": 12345}`,
   184  					want: map[string]string{
   185  						"foo": "hello",
   186  						"bar": "world",
   187  					},
   188  				},
   189  				{
   190  					name:  "unused is int",
   191  					input: `{"foo": "hello", "bar": "world", "unused": 12345}`,
   192  					want: map[string]string{
   193  						"foo": "hello",
   194  						"bar": "world",
   195  					},
   196  				},
   197  				{
   198  					name:  "required is null",
   199  					input: `{"foo": null, "bar": 12345, "baz": "nooo"}`,
   200  					want:  nil,
   201  				},
   202  				{
   203  					name:  "required is bool",
   204  					input: `{"foo": false, "bar": 12345, "baz": "nooo"}`,
   205  					want:  nil,
   206  				},
   207  				{
   208  					name:  "required is array",
   209  					input: `{"foo": [1, 2, 3], "bar": 12345, "baz": "nooo"}`,
   210  					want:  nil,
   211  				},
   212  				{
   213  					name:  "required is object",
   214  					input: `{"foo": {"a": "b"}, "bar": 12345, "baz": "nooo"}`,
   215  					want:  nil,
   216  				},
   217  				{
   218  					name:  "optional is null",
   219  					input: `{"foo": "hello", "bar": "world", "baz": null}`,
   220  					want: map[string]string{
   221  						"foo": "hello",
   222  						"bar": "world",
   223  					},
   224  				},
   225  				{
   226  					name:  "optional is bool",
   227  					input: `{"foo": "hello", "bar": "world", "baz": true}`,
   228  					want: map[string]string{
   229  						"foo": "hello",
   230  						"bar": "world",
   231  					},
   232  				},
   233  				{
   234  					name:  "optional is array",
   235  					input: `{"foo": "hello", "bar": "world", "baz": [1, 2, 3]}`,
   236  					want: map[string]string{
   237  						"foo": "hello",
   238  						"bar": "world",
   239  					},
   240  				},
   241  				{
   242  					name:  "optional is object",
   243  					input: `{"foo": "hello", "bar": "world", "baz": {"a": "b"}}`,
   244  					want: map[string]string{
   245  						"foo": "hello",
   246  						"bar": "world",
   247  					},
   248  				},
   249  			},
   250  		},
   251  		{
   252  			name:     "robustness checks",
   253  			required: []string{"foo", "bar"},
   254  			optional: []string{"baz"},
   255  			subs: []testExtractorSubCase{
   256  				{
   257  					name:  "order independent",
   258  					input: `{"baz": "12345", "bar": "world", "foo": "hello"}`,
   259  					want: map[string]string{
   260  						"foo": "hello",
   261  						"bar": "world",
   262  						"baz": "12345",
   263  					},
   264  				},
   265  				{
   266  					// This is not valid JSON. The extractor can still parse it, however.
   267  					name:  "trailing comma",
   268  					input: `{"foo": "hello", "bar": "world", "baz": "12345",}`,
   269  					want: map[string]string{
   270  						"foo": "hello",
   271  						"bar": "world",
   272  						"baz": "12345",
   273  					},
   274  				},
   275  				{
   276  					name:  "nested",
   277  					input: `{"key": {"baz": "12345", "bar": "world", "foo": "hello"}}`,
   278  					want: map[string]string{
   279  						"foo": "hello",
   280  						"bar": "world",
   281  						"baz": "12345",
   282  					},
   283  				},
   284  				{
   285  					name: "multiline",
   286  					input: `{
   287  	"baz": "12345",
   288  	"bar": "world",
   289  	"foo": "hello"
   290  }`,
   291  					want: map[string]string{
   292  						"foo": "hello",
   293  						"bar": "world",
   294  						"baz": "12345",
   295  					},
   296  				},
   297  				{
   298  					// This is not valid JSON. The extractor can still parse it, however.
   299  					name: "multiline_trailing_comma",
   300  					input: `{
   301  	"baz": "12345",
   302  	"bar": "world",
   303  	"foo": "hello",
   304  }`,
   305  					want: map[string]string{
   306  						"foo": "hello",
   307  						"bar": "world",
   308  						"baz": "12345",
   309  					},
   310  				},
   311  				{
   312  					name:  "escaped",
   313  					input: `"{\n  \"foo\": \"hello\",\n  \"bar\": \"world\"\n}"`,
   314  					want: map[string]string{
   315  						"foo": "hello",
   316  						"bar": "world",
   317  					},
   318  				},
   319  				{
   320  					name:  "twice escaped",
   321  					input: `"\"{\\n  \\\"foo\\\": \\\"hello\\\",\\n  \\\"bar\\\": \\\"world\\\"\\n}"`,
   322  					want: map[string]string{
   323  						"foo": "hello",
   324  						"bar": "world",
   325  					},
   326  				},
   327  				{
   328  					name:  "four times escaped",
   329  					input: `"\"\\\"\\\\\\\"{\\\\\\\\\\\\\\\"foo\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"hello-world\\\\\\\\\\\\\\\",\\\\\\\\\\\\\\\"bar\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"my@friend\\\\\\\\\\\\\\\"}\\\\\\\"\\\"\""`,
   330  					want: map[string]string{
   331  						"foo": "hello-world",
   332  						"bar": "my@friend",
   333  					},
   334  				},
   335  				{
   336  					name:  "preserves whitespace in value",
   337  					input: `{"foo": "hello\nworld", "bar": "my friend"}`,
   338  					want: map[string]string{
   339  						"foo": "hello\nworld",
   340  						"bar": "my friend",
   341  					},
   342  				},
   343  				{
   344  					name:  "preserves whitespace in value when escaped",
   345  					input: `"{\n  \"foo\": \"hello\\nworld\",\n  \"bar\": \"my friend\"\n}"`,
   346  					want: map[string]string{
   347  						"foo": "hello\nworld",
   348  						"bar": "my friend",
   349  					},
   350  				},
   351  				{
   352  					name: "different_whitespace_after_colon",
   353  					input: `{
   354  	"baz":  "12345",
   355  	"bar":	"world",
   356  	"foo":
   357  	"hello"
   358  }`,
   359  					want: map[string]string{
   360  						"foo": "hello",
   361  						"bar": "world",
   362  						"baz": "12345",
   363  					},
   364  				},
   365  				{
   366  					name:  "surrounding braces not required",
   367  					input: `"foo": "hello", "bar": "world", "baz": "12345",`,
   368  					want: map[string]string{
   369  						"foo": "hello",
   370  						"bar": "world",
   371  						"baz": "12345",
   372  					},
   373  				},
   374  			},
   375  		},
   376  		{
   377  			name:     "limitations",
   378  			required: []string{"foo"},
   379  			optional: []string{},
   380  			subs: []testExtractorSubCase{
   381  				{
   382  					name:  "single quotes for key",
   383  					input: `{'foo': "hello"}`,
   384  					want:  nil,
   385  				},
   386  				{
   387  					name:  "single quotes for value",
   388  					input: `{"foo": 'hello'}`,
   389  					want:  nil,
   390  				},
   391  				{
   392  					name:  "single quotes for both",
   393  					input: `{'foo': 'hello'}`,
   394  					want:  nil,
   395  				},
   396  				{
   397  					name:  "separator not colon",
   398  					input: `"foo"="hello"`,
   399  					want:  nil,
   400  				},
   401  			},
   402  		},
   403  	}
   404  	for _, tc := range cases {
   405  		t.Run(tc.name, func(t *testing.T) {
   406  			t.Parallel()
   407  			ex := flatjson.NewExtractor(tc.required, tc.optional)
   408  			for _, sc := range tc.subs {
   409  				t.Run(sc.name, func(t *testing.T) {
   410  					t.Parallel()
   411  					got := ex.Extract([]byte(sc.input))
   412  					if diff := cmp.Diff(sc.want, got); diff != "" {
   413  						t.Errorf("Extract() diff (-want +got):\n%s", diff)
   414  					}
   415  				})
   416  			}
   417  		})
   418  	}
   419  }