github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/jsonpath/jsonpath_test.go (about)

     1  /*
     2  Copyright 2015 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package jsonpath
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"reflect"
    24  	"sort"
    25  	"strings"
    26  	"testing"
    27  )
    28  
    29  type jsonpathTest struct {
    30  	name        string
    31  	template    string
    32  	input       interface{}
    33  	expect      string
    34  	expectError bool
    35  }
    36  
    37  func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) {
    38  	for _, test := range tests {
    39  		j := New(test.name)
    40  		j.AllowMissingKeys(allowMissingKeys)
    41  		err := j.Parse(test.template)
    42  		if err != nil {
    43  			if !test.expectError {
    44  				t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
    45  			}
    46  			continue
    47  		}
    48  		buf := new(bytes.Buffer)
    49  		err = j.Execute(buf, test.input)
    50  		if test.expectError {
    51  			if err == nil {
    52  				t.Errorf("in %s, expected execute error", test.name)
    53  			}
    54  			continue
    55  		} else if err != nil {
    56  			t.Errorf("in %s, execute error %v", test.name, err)
    57  		}
    58  		out := buf.String()
    59  		if out != test.expect {
    60  			t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out)
    61  		}
    62  	}
    63  }
    64  
    65  // testJSONPathSortOutput test cases related to map, the results may print in random order
    66  func testJSONPathSortOutput(tests []jsonpathTest, t *testing.T) {
    67  	for _, test := range tests {
    68  		j := New(test.name)
    69  		err := j.Parse(test.template)
    70  		if err != nil {
    71  			t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
    72  		}
    73  		buf := new(bytes.Buffer)
    74  		err = j.Execute(buf, test.input)
    75  		if err != nil {
    76  			t.Errorf("in %s, execute error %v", test.name, err)
    77  		}
    78  		out := buf.String()
    79  		//since map is visited in random order, we need to sort the results.
    80  		sortedOut := strings.Fields(out)
    81  		sort.Strings(sortedOut)
    82  		sortedExpect := strings.Fields(test.expect)
    83  		sort.Strings(sortedExpect)
    84  		if !reflect.DeepEqual(sortedOut, sortedExpect) {
    85  			t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out)
    86  		}
    87  	}
    88  }
    89  
    90  func testFailJSONPath(tests []jsonpathTest, t *testing.T) {
    91  	for _, test := range tests {
    92  		j := New(test.name)
    93  		err := j.Parse(test.template)
    94  		if err != nil {
    95  			t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
    96  		}
    97  		buf := new(bytes.Buffer)
    98  		err = j.Execute(buf, test.input)
    99  		var out string
   100  		if err == nil {
   101  			out = "nil"
   102  		} else {
   103  			out = err.Error()
   104  		}
   105  		if out != test.expect {
   106  			t.Errorf("in %s, expect to get error %q, got %q", test.name, test.expect, out)
   107  		}
   108  	}
   109  }
   110  
   111  type book struct {
   112  	Category string
   113  	Author   string
   114  	Title    string
   115  	Price    float32
   116  }
   117  
   118  func (b book) String() string {
   119  	return fmt.Sprintf("{Category: %s, Author: %s, Title: %s, Price: %v}", b.Category, b.Author, b.Title, b.Price)
   120  }
   121  
   122  type bicycle struct {
   123  	Color string
   124  	Price float32
   125  	IsNew bool
   126  }
   127  
   128  type empName string
   129  type job string
   130  type store struct {
   131  	Book      []book
   132  	Bicycle   []bicycle
   133  	Name      string
   134  	Labels    map[string]int
   135  	Employees map[empName]job
   136  }
   137  
   138  func TestStructInput(t *testing.T) {
   139  
   140  	storeData := store{
   141  		Name: "jsonpath",
   142  		Book: []book{
   143  			{"reference", "Nigel Rees", "Sayings of the Centurey", 8.95},
   144  			{"fiction", "Evelyn Waugh", "Sword of Honor", 12.99},
   145  			{"fiction", "Herman Melville", "Moby Dick", 8.99},
   146  		},
   147  		Bicycle: []bicycle{
   148  			{"red", 19.95, true},
   149  			{"green", 20.01, false},
   150  		},
   151  		Labels: map[string]int{
   152  			"engineer": 10,
   153  			"web/html": 15,
   154  			"k8s-app":  20,
   155  		},
   156  		Employees: map[empName]job{
   157  			"jason": "manager",
   158  			"dan":   "clerk",
   159  		},
   160  	}
   161  
   162  	storeTests := []jsonpathTest{
   163  		{"plain", "hello jsonpath", nil, "hello jsonpath", false},
   164  		{"recursive", "{..}", []int{1, 2, 3}, "[1 2 3]", false},
   165  		{"filter", "{[?(@<5)]}", []int{2, 6, 3, 7}, "2 3", false},
   166  		{"quote", `{"{"}`, nil, "{", false},
   167  		{"union", "{[1,3,4]}", []int{0, 1, 2, 3, 4}, "1 3 4", false},
   168  		{"array", "{[0:2]}", []string{"Monday", "Tudesday"}, "Monday Tudesday", false},
   169  		{"variable", "hello {.Name}", storeData, "hello jsonpath", false},
   170  		{"dict/", "{$.Labels.web/html}", storeData, "15", false},
   171  		{"dict/", "{$.Employees.jason}", storeData, "manager", false},
   172  		{"dict/", "{$.Employees.dan}", storeData, "clerk", false},
   173  		{"dict-", "{.Labels.k8s-app}", storeData, "20", false},
   174  		{"nest", "{.Bicycle[*].Color}", storeData, "red green", false},
   175  		{"allarray", "{.Book[*].Author}", storeData, "Nigel Rees Evelyn Waugh Herman Melville", false},
   176  		{"allfileds", "{.Bicycle.*}", storeData, "{red 19.95 true} {green 20.01 false}", false},
   177  		{"recurfileds", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false},
   178  		{"lastarray", "{.Book[-1:]}", storeData,
   179  			"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false},
   180  		{"recurarray", "{..Book[2]}", storeData,
   181  			"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false},
   182  		{"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, "{red 19.95 true}", false},
   183  	}
   184  	testJSONPath(storeTests, false, t)
   185  
   186  	missingKeyTests := []jsonpathTest{
   187  		{"nonexistent field", "{.hello}", storeData, "", false},
   188  	}
   189  	testJSONPath(missingKeyTests, true, t)
   190  
   191  	failStoreTests := []jsonpathTest{
   192  		{"invalid identifier", "{hello}", storeData, "unrecognized identifier hello", false},
   193  		{"nonexistent field", "{.hello}", storeData, "hello is not found", false},
   194  		{"invalid array", "{.Labels[0]}", storeData, "map[string]int is not array or slice", false},
   195  		{"invalid filter operator", "{.Book[?(@.Price<>10)]}", storeData, "unrecognized filter operator <>", false},
   196  		{"redundant end", "{range .Labels.*}{@}{end}{end}", storeData, "not in range, nothing to end", false},
   197  	}
   198  	testFailJSONPath(failStoreTests, t)
   199  }
   200  
   201  func TestJSONInput(t *testing.T) {
   202  	var pointsJSON = []byte(`[
   203  		{"id": "i1", "x":4, "y":-5},
   204  		{"id": "i2", "x":-2, "y":-5, "z":1},
   205  		{"id": "i3", "x":  8, "y":  3 },
   206  		{"id": "i4", "x": -6, "y": -1 },
   207  		{"id": "i5", "x":  0, "y":  2, "z": 1 },
   208  		{"id": "i6", "x":  1, "y":  4 }
   209  	]`)
   210  	var pointsData interface{}
   211  	err := json.Unmarshal(pointsJSON, &pointsData)
   212  	if err != nil {
   213  		t.Error(err)
   214  	}
   215  	pointsTests := []jsonpathTest{
   216  		{"exists filter", "{[?(@.z)].id}", pointsData, "i2 i5", false},
   217  		{"bracket key", "{[0]['id']}", pointsData, "i1", false},
   218  	}
   219  	testJSONPath(pointsTests, false, t)
   220  }
   221  
   222  // TestKubernetes tests some use cases from kubernetes
   223  func TestKubernetes(t *testing.T) {
   224  	var input = []byte(`{
   225  	  "kind": "List",
   226  	  "items":[
   227  		{
   228  		  "kind":"None",
   229  		  "metadata":{
   230  		    "name":"127.0.0.1",
   231  			"labels":{
   232  			  "kubernetes.io/hostname":"127.0.0.1"
   233  			}
   234  		  },
   235  		  "status":{
   236  			"capacity":{"cpu":"4"},
   237  			"ready": true,
   238  			"addresses":[{"type": "LegacyHostIP", "address":"127.0.0.1"}]
   239  		  }
   240  		},
   241  		{
   242  		  "kind":"None",
   243  		  "metadata":{
   244  			"name":"127.0.0.2",
   245  			"labels":{
   246  			  "kubernetes.io/hostname":"127.0.0.2"
   247  			}
   248  		  },
   249  		  "status":{
   250  			"capacity":{"cpu":"8"},
   251  			"ready": false,
   252  			"addresses":[
   253  			  {"type": "LegacyHostIP", "address":"127.0.0.2"},
   254  			  {"type": "another", "address":"127.0.0.3"}
   255  			]
   256  		  }
   257  		}
   258  	  ],
   259  	  "users":[
   260  	    {
   261  	      "name": "myself",
   262  	      "user": {}
   263  	    },
   264  	    {
   265  	      "name": "e2e",
   266  	      "user": {"username": "admin", "password": "secret"}
   267  	  	}
   268  	  ]
   269  	}`)
   270  	var nodesData interface{}
   271  	err := json.Unmarshal(input, &nodesData)
   272  	if err != nil {
   273  		t.Error(err)
   274  	}
   275  
   276  	nodesTests := []jsonpathTest{
   277  		{"range item", `{range .items[*]}{.metadata.name}, {end}{.kind}`, nodesData, "127.0.0.1, 127.0.0.2, List", false},
   278  		{"range item with quote", `{range .items[*]}{.metadata.name}{"\t"}{end}`, nodesData, "127.0.0.1\t127.0.0.2\t", false},
   279  		{"range address", `{.items[*].status.addresses[*].address}`, nodesData,
   280  			"127.0.0.1 127.0.0.2 127.0.0.3", false},
   281  		{"double range", `{range .items[*]}{range .status.addresses[*]}{.address}, {end}{end}`, nodesData,
   282  			"127.0.0.1, 127.0.0.2, 127.0.0.3, ", false},
   283  		{"item name", `{.items[*].metadata.name}`, nodesData, "127.0.0.1 127.0.0.2", false},
   284  		{"union nodes capacity", `{.items[*]['metadata.name', 'status.capacity']}`, nodesData,
   285  			"127.0.0.1 127.0.0.2 map[cpu:4] map[cpu:8]", false},
   286  		{"range nodes capacity", `{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}`, nodesData,
   287  			"[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]] ", false},
   288  		{"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret", false},
   289  		{"hostname", `{.items[0].metadata.labels.kubernetes\.io/hostname}`, &nodesData, "127.0.0.1", false},
   290  		{"hostname filter", `{.items[?(@.metadata.labels.kubernetes\.io/hostname=="127.0.0.1")].kind}`, &nodesData, "None", false},
   291  		{"bool item", `{.items[?(@..ready==true)].metadata.name}`, &nodesData, "127.0.0.1", false},
   292  	}
   293  	testJSONPath(nodesTests, false, t)
   294  
   295  	randomPrintOrderTests := []jsonpathTest{
   296  		{"recursive name", "{..name}", nodesData, `127.0.0.1 127.0.0.2 myself e2e`, false},
   297  	}
   298  	testJSONPathSortOutput(randomPrintOrderTests, t)
   299  }
   300  
   301  func TestFilterPartialMatchesSometimesMissingAnnotations(t *testing.T) {
   302  	// for https://issues.k8s.io/45546
   303  	var input = []byte(`{
   304  		"kind": "List",
   305  		"items": [
   306  			{
   307  				"kind": "Pod",
   308  				"metadata": {
   309  					"name": "pod1",
   310  					"annotations": {
   311  						"color": "blue"
   312  					}
   313  				}
   314  			},
   315  			{
   316  				"kind": "Pod",
   317  				"metadata": {
   318  					"name": "pod2"
   319  				}
   320  			},
   321  			{
   322  				"kind": "Pod",
   323  				"metadata": {
   324  					"name": "pod3",
   325  					"annotations": {
   326  						"color": "green"
   327  					}
   328  				}
   329  			},
   330  			{
   331  				"kind": "Pod",
   332  				"metadata": {
   333  					"name": "pod4",
   334  					"annotations": {
   335  						"color": "blue"
   336  					}
   337  				}
   338  			}
   339  		]
   340  	}`)
   341  	var data interface{}
   342  	err := json.Unmarshal(input, &data)
   343  	if err != nil {
   344  		t.Fatal(err)
   345  	}
   346  
   347  	testJSONPath(
   348  		[]jsonpathTest{
   349  			{
   350  				"filter, should only match a subset, some items don't have annotations, tolerate missing items",
   351  				`{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`,
   352  				data,
   353  				"pod1 pod4",
   354  				false, // expect no error
   355  			},
   356  		},
   357  		true, // allow missing keys
   358  		t,
   359  	)
   360  
   361  	testJSONPath(
   362  		[]jsonpathTest{
   363  			{
   364  				"filter, should only match a subset, some items don't have annotations, error on missing items",
   365  				`{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`,
   366  				data,
   367  				"",
   368  				true, // expect an error
   369  			},
   370  		},
   371  		false, // don't allow missing keys
   372  		t,
   373  	)
   374  }
   375  
   376  func TestNegativeIndex(t *testing.T) {
   377  	var input = []byte(
   378  		`{
   379  			"apiVersion": "v1",
   380  			"kind": "Pod",
   381  			"spec": {
   382  				"containers": [
   383  					{
   384  						"image": "radial/busyboxplus:curl",
   385  						"name": "fake0"
   386  					},
   387  					{
   388  						"image": "radial/busyboxplus:curl",
   389  						"name": "fake1"
   390  					},
   391  					{
   392  						"image": "radial/busyboxplus:curl",
   393  						"name": "fake2"
   394  					},
   395  					{
   396  						"image": "radial/busyboxplus:curl",
   397  						"name": "fake3"
   398  					}]}}`)
   399  
   400  	var data interface{}
   401  	err := json.Unmarshal(input, &data)
   402  	if err != nil {
   403  		t.Fatal(err)
   404  	}
   405  
   406  	testJSONPath(
   407  		[]jsonpathTest{
   408  			{
   409  				"test containers[0], it equals containers[0]",
   410  				`{.spec.containers[0].name}`,
   411  				data,
   412  				"fake0",
   413  				false,
   414  			},
   415  			{
   416  				"test containers[0:0], it equals the empty set",
   417  				`{.spec.containers[0:0].name}`,
   418  				data,
   419  				"",
   420  				false,
   421  			},
   422  			{
   423  				"test containers[0:-1], it equals containers[0:3]",
   424  				`{.spec.containers[0:-1].name}`,
   425  				data,
   426  				"fake0 fake1 fake2",
   427  				false,
   428  			},
   429  			{
   430  				"test containers[-1:0], expect error",
   431  				`{.spec.containers[-1:0].name}`,
   432  				data,
   433  				"",
   434  				true,
   435  			},
   436  			{
   437  				"test containers[-1], it equals containers[3]",
   438  				`{.spec.containers[-1].name}`,
   439  				data,
   440  				"fake3",
   441  				false,
   442  			},
   443  			{
   444  				"test containers[-1:], it equals containers[3:]",
   445  				`{.spec.containers[-1:].name}`,
   446  				data,
   447  				"fake3",
   448  				false,
   449  			},
   450  			{
   451  				"test containers[-2], it equals containers[2]",
   452  				`{.spec.containers[-2].name}`,
   453  				data,
   454  				"fake2",
   455  				false,
   456  			},
   457  			{
   458  				"test containers[-2:], it equals containers[2:]",
   459  				`{.spec.containers[-2:].name}`,
   460  				data,
   461  				"fake2 fake3",
   462  				false,
   463  			},
   464  			{
   465  				"test containers[-3], it equals containers[1]",
   466  				`{.spec.containers[-3].name}`,
   467  				data,
   468  				"fake1",
   469  				false,
   470  			},
   471  			{
   472  				"test containers[-4], it equals containers[0]",
   473  				`{.spec.containers[-4].name}`,
   474  				data,
   475  				"fake0",
   476  				false,
   477  			},
   478  			{
   479  				"test containers[-4:], it equals containers[0:]",
   480  				`{.spec.containers[-4:].name}`,
   481  				data,
   482  				"fake0 fake1 fake2 fake3",
   483  				false,
   484  			},
   485  			{
   486  				"test containers[-5], expect a error cause it out of bounds",
   487  				`{.spec.containers[-5].name}`,
   488  				data,
   489  				"",
   490  				true, // expect error
   491  			},
   492  			{
   493  				"test containers[5:5], expect empty set",
   494  				`{.spec.containers[5:5].name}`,
   495  				data,
   496  				"",
   497  				false,
   498  			},
   499  			{
   500  				"test containers[-5:-5], expect empty set",
   501  				`{.spec.containers[-5:-5].name}`,
   502  				data,
   503  				"",
   504  				false,
   505  			},
   506  			{
   507  				"test containers[3:1], expect a error cause start index is greater than end index",
   508  				`{.spec.containers[3:1].name}`,
   509  				data,
   510  				"",
   511  				true,
   512  			},
   513  			{
   514  				"test containers[-1:-2], it equals containers[3:2], expect a error cause start index is greater than end index",
   515  				`{.spec.containers[-1:-2].name}`,
   516  				data,
   517  				"",
   518  				true,
   519  			},
   520  		},
   521  		false,
   522  		t,
   523  	)
   524  }
   525  
   526  func TestStep(t *testing.T) {
   527  	var input = []byte(
   528  		`{
   529  			"apiVersion": "v1",
   530  			"kind": "Pod",
   531  			"spec": {
   532  				"containers": [
   533  					{
   534  						"image": "radial/busyboxplus:curl",
   535  						"name": "fake0"
   536  					},
   537  					{
   538  						"image": "radial/busyboxplus:curl",
   539  						"name": "fake1"
   540  					},
   541  					{
   542  						"image": "radial/busyboxplus:curl",
   543  						"name": "fake2"
   544  					},
   545  					{
   546  						"image": "radial/busyboxplus:curl",
   547  						"name": "fake3"
   548  					},
   549  					{
   550  						"image": "radial/busyboxplus:curl",
   551  						"name": "fake4"
   552  					},
   553  					{
   554  						"image": "radial/busyboxplus:curl",
   555  						"name": "fake5"
   556  					}]}}`)
   557  
   558  	var data interface{}
   559  	err := json.Unmarshal(input, &data)
   560  	if err != nil {
   561  		t.Fatal(err)
   562  	}
   563  
   564  	testJSONPath(
   565  		[]jsonpathTest{
   566  			{
   567  				"test containers[0:], it equals containers[0:6:1]",
   568  				`{.spec.containers[0:].name}`,
   569  				data,
   570  				"fake0 fake1 fake2 fake3 fake4 fake5",
   571  				false,
   572  			},
   573  			{
   574  				"test containers[0:6:], it equals containers[0:6:1]",
   575  				`{.spec.containers[0:6:].name}`,
   576  				data,
   577  				"fake0 fake1 fake2 fake3 fake4 fake5",
   578  				false,
   579  			},
   580  			{
   581  				"test containers[0:6:1]",
   582  				`{.spec.containers[0:6:1].name}`,
   583  				data,
   584  				"fake0 fake1 fake2 fake3 fake4 fake5",
   585  				false,
   586  			},
   587  			{
   588  				"test containers[0:6:0], it errors",
   589  				`{.spec.containers[0:6:0].name}`,
   590  				data,
   591  				"",
   592  				true,
   593  			},
   594  			{
   595  				"test containers[0:6:-1], it errors",
   596  				`{.spec.containers[0:6:-1].name}`,
   597  				data,
   598  				"",
   599  				true,
   600  			},
   601  			{
   602  				"test containers[1:4:2]",
   603  				`{.spec.containers[1:4:2].name}`,
   604  				data,
   605  				"fake1 fake3",
   606  				false,
   607  			},
   608  			{
   609  				"test containers[1:4:3]",
   610  				`{.spec.containers[1:4:3].name}`,
   611  				data,
   612  				"fake1",
   613  				false,
   614  			},
   615  			{
   616  				"test containers[1:4:4]",
   617  				`{.spec.containers[1:4:4].name}`,
   618  				data,
   619  				"fake1",
   620  				false,
   621  			},
   622  			{
   623  				"test containers[0:6:2]",
   624  				`{.spec.containers[0:6:2].name}`,
   625  				data,
   626  				"fake0 fake2 fake4",
   627  				false,
   628  			},
   629  			{
   630  				"test containers[0:6:3]",
   631  				`{.spec.containers[0:6:3].name}`,
   632  				data,
   633  				"fake0 fake3",
   634  				false,
   635  			},
   636  			{
   637  				"test containers[0:6:5]",
   638  				`{.spec.containers[0:6:5].name}`,
   639  				data,
   640  				"fake0 fake5",
   641  				false,
   642  			},
   643  			{
   644  				"test containers[0:6:6]",
   645  				`{.spec.containers[0:6:6].name}`,
   646  				data,
   647  				"fake0",
   648  				false,
   649  			},
   650  		},
   651  		false,
   652  		t,
   653  	)
   654  }