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 }