github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/gnmi/operation_test.go (about)

     1  // Copyright (c) 2017 Arista Networks, Inc.
     2  // Use of this source code is governed by the Apache License 2.0
     3  // that can be found in the COPYING file.
     4  
     5  package gnmi
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"io/ioutil"
    11  	"os"
    12  	"testing"
    13  
    14  	"github.com/aristanetworks/goarista/test"
    15  	"google.golang.org/protobuf/proto"
    16  	"google.golang.org/protobuf/types/known/anypb"
    17  
    18  	pb "github.com/openconfig/gnmi/proto/gnmi"
    19  )
    20  
    21  func TestNewSetRequest(t *testing.T) {
    22  	pathFoo := &pb.Path{
    23  		Element: []string{"foo"},
    24  		Elem:    []*pb.PathElem{{Name: "foo"}},
    25  	}
    26  	pathCli := &pb.Path{
    27  		Origin: "cli",
    28  	}
    29  	pathP4 := &pb.Path{
    30  		Origin: "p4_config",
    31  	}
    32  	pathOC := &pb.Path{
    33  		Origin: "openconfig",
    34  	}
    35  
    36  	fileData := []struct {
    37  		name     string
    38  		fileName string
    39  		content  string
    40  	}{{
    41  		name:     "p4_config",
    42  		fileName: "p4TestFile",
    43  		content:  "p4_config test",
    44  	}, {
    45  		name:     "originCLIFileData",
    46  		fileName: "originCLIFile",
    47  		content: `enable
    48  configure
    49  hostname new`,
    50  	}}
    51  
    52  	fileNames := make([]string, 2, 2)
    53  	for i, data := range fileData {
    54  		f, err := ioutil.TempFile("", data.name)
    55  		if err != nil {
    56  			t.Errorf("cannot create test file for %s", data.name)
    57  		}
    58  		filename := f.Name()
    59  		defer os.Remove(filename)
    60  		fileNames[i] = filename
    61  		if _, err := f.WriteString(data.content); err != nil {
    62  			t.Errorf("cannot write test file for %s", data.name)
    63  		}
    64  		f.Close()
    65  	}
    66  
    67  	testCases := map[string]struct {
    68  		setOps []*Operation
    69  		exp    *pb.SetRequest
    70  	}{
    71  		"delete": {
    72  			setOps: []*Operation{{Type: "delete", Path: []string{"foo"}}},
    73  			exp:    &pb.SetRequest{Delete: []*pb.Path{pathFoo}},
    74  		},
    75  		"update": {
    76  			setOps: []*Operation{{Type: "update", Path: []string{"foo"}, Val: "true"}},
    77  			exp: &pb.SetRequest{
    78  				Update: []*pb.Update{{
    79  					Path: pathFoo,
    80  					Val: &pb.TypedValue{
    81  						Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}},
    82  				}},
    83  			},
    84  		},
    85  		"replace": {
    86  			setOps: []*Operation{{Type: "replace", Path: []string{"foo"}, Val: "true"}},
    87  			exp: &pb.SetRequest{
    88  				Replace: []*pb.Update{{
    89  					Path: pathFoo,
    90  					Val: &pb.TypedValue{
    91  						Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}},
    92  				}},
    93  			},
    94  		},
    95  		"cli-replace": {
    96  			setOps: []*Operation{{Type: "replace", Origin: "cli",
    97  				Val: "hostname foo\nip routing"}},
    98  			exp: &pb.SetRequest{
    99  				Replace: []*pb.Update{{
   100  					Path: pathCli,
   101  					Val: &pb.TypedValue{
   102  						Value: &pb.TypedValue_AsciiVal{AsciiVal: "hostname foo\nip routing"}},
   103  				}},
   104  			},
   105  		},
   106  		"p4_config": {
   107  			setOps: []*Operation{{Type: "replace", Origin: "p4_config",
   108  				Val: fileNames[0]}},
   109  			exp: &pb.SetRequest{
   110  				Replace: []*pb.Update{{
   111  					Path: pathP4,
   112  					Val: &pb.TypedValue{
   113  						Value: &pb.TypedValue_ProtoBytes{ProtoBytes: []byte(fileData[0].content)}},
   114  				}},
   115  			},
   116  		},
   117  		"target": {
   118  			setOps: []*Operation{{Type: "replace", Target: "JPE1234567",
   119  				Path: []string{"foo"}, Val: "true"}},
   120  			exp: &pb.SetRequest{
   121  				Prefix: &pb.Path{Target: "JPE1234567"},
   122  				Replace: []*pb.Update{{
   123  					Path: pathFoo,
   124  					Val: &pb.TypedValue{
   125  						Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}},
   126  				}},
   127  			},
   128  		},
   129  		"openconfig origin": {
   130  			setOps: []*Operation{{Type: "replace", Origin: "openconfig",
   131  				Val: "true"}},
   132  			exp: &pb.SetRequest{
   133  				Replace: []*pb.Update{{
   134  					Path: pathOC,
   135  					Val: &pb.TypedValue{
   136  						Value: &pb.TypedValue_JsonIetfVal{
   137  							JsonIetfVal: []byte("true"),
   138  						},
   139  					},
   140  				}},
   141  			},
   142  		},
   143  		"originCLI file": {
   144  			setOps: []*Operation{{Type: "update", Origin: "cli",
   145  				Val: fileNames[1]}},
   146  			exp: &pb.SetRequest{
   147  				Update: []*pb.Update{{
   148  					Path: pathCli,
   149  					Val: &pb.TypedValue{
   150  						Value: &pb.TypedValue_AsciiVal{AsciiVal: fileData[1].content}},
   151  				}},
   152  			},
   153  		},
   154  		"union_replace": {
   155  			setOps: []*Operation{{Type: "union_replace", Path: []string{"foo"}, Val: "true"}},
   156  			exp: &pb.SetRequest{
   157  				UnionReplace: []*pb.Update{{
   158  					Path: pathFoo,
   159  					Val: &pb.TypedValue{
   160  						Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}},
   161  				}},
   162  			},
   163  		},
   164  		"union_replace openconfig and cli origin": {
   165  			setOps: []*Operation{{Type: "union_replace", Origin: "openconfig", Val: "true"},
   166  				{Type: "union_replace", Origin: "cli", Val: fileNames[1]}},
   167  			exp: &pb.SetRequest{
   168  				UnionReplace: []*pb.Update{{
   169  					Path: pathOC,
   170  					Val: &pb.TypedValue{
   171  						Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}},
   172  				}, {
   173  					Path: pathCli,
   174  					Val: &pb.TypedValue{
   175  						Value: &pb.TypedValue_AsciiVal{AsciiVal: fileData[1].content}},
   176  				}},
   177  			},
   178  		},
   179  	}
   180  
   181  	for name, tc := range testCases {
   182  		t.Run(name, func(t *testing.T) {
   183  			got, err := newSetRequest(tc.setOps)
   184  			if err != nil {
   185  				t.Fatal(err)
   186  			}
   187  			if !proto.Equal(tc.exp, got) {
   188  				t.Errorf("Exp: %v Got: %v", tc.exp, got)
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  func TestStrUpdateVal(t *testing.T) {
   195  	anyBytes, err := proto.Marshal(&pb.ModelData{Name: "foobar"})
   196  	if err != nil {
   197  		t.Fatal(err)
   198  	}
   199  	anyMessage := &anypb.Any{TypeUrl: "gnmi/ModelData", Value: anyBytes}
   200  
   201  	for name, tc := range map[string]struct {
   202  		update *pb.Update
   203  		exp    string
   204  	}{
   205  		"JSON Value": {
   206  			update: &pb.Update{
   207  				Value: &pb.Value{
   208  					Value: []byte(`{"foo":"bar"}`),
   209  					Type:  pb.Encoding_JSON}},
   210  			exp: `{"foo":"bar"}`,
   211  		},
   212  		"JSON_IETF Value": {
   213  			update: &pb.Update{
   214  				Value: &pb.Value{
   215  					Value: []byte(`{"foo":"bar"}`),
   216  					Type:  pb.Encoding_JSON_IETF}},
   217  			exp: `{"foo":"bar"}`,
   218  		},
   219  		"BYTES Value": {
   220  			update: &pb.Update{
   221  				Value: &pb.Value{
   222  					Value: []byte{0xde, 0xad},
   223  					Type:  pb.Encoding_BYTES}},
   224  			exp: "3q0=",
   225  		},
   226  		"PROTO Value": {
   227  			update: &pb.Update{
   228  				Value: &pb.Value{
   229  					Value: []byte{0xde, 0xad},
   230  					Type:  pb.Encoding_PROTO}},
   231  			exp: "3q0=",
   232  		},
   233  		"ASCII Value": {
   234  			update: &pb.Update{
   235  				Value: &pb.Value{
   236  					Value: []byte("foobar"),
   237  					Type:  pb.Encoding_ASCII}},
   238  			exp: "foobar",
   239  		},
   240  		"INVALID Value": {
   241  			update: &pb.Update{
   242  				Value: &pb.Value{
   243  					Value: []byte("foobar"),
   244  					Type:  pb.Encoding(42)}},
   245  			exp: "foobar",
   246  		},
   247  		"StringVal": {
   248  			update: &pb.Update{Val: &pb.TypedValue{
   249  				Value: &pb.TypedValue_StringVal{StringVal: "foobar"}}},
   250  			exp: "foobar",
   251  		},
   252  		"IntVal": {
   253  			update: &pb.Update{Val: &pb.TypedValue{
   254  				Value: &pb.TypedValue_IntVal{IntVal: -42}}},
   255  			exp: "-42",
   256  		},
   257  		"UintVal": {
   258  			update: &pb.Update{Val: &pb.TypedValue{
   259  				Value: &pb.TypedValue_UintVal{UintVal: 42}}},
   260  			exp: "42",
   261  		},
   262  		"BoolVal": {
   263  			update: &pb.Update{Val: &pb.TypedValue{
   264  				Value: &pb.TypedValue_BoolVal{BoolVal: true}}},
   265  			exp: "true",
   266  		},
   267  		"BytesVal": {
   268  			update: &pb.Update{Val: &pb.TypedValue{
   269  				Value: &pb.TypedValue_BytesVal{BytesVal: []byte{0xde, 0xad}}}},
   270  			exp: "3q0=",
   271  		},
   272  		"FloatVal": {
   273  			update: &pb.Update{Val: &pb.TypedValue{
   274  				Value: &pb.TypedValue_FloatVal{FloatVal: 3.14}}},
   275  			exp: "3.14",
   276  		},
   277  		"DoubleVal": {
   278  			update: &pb.Update{Val: &pb.TypedValue{
   279  				Value: &pb.TypedValue_DoubleVal{DoubleVal: 3.14}}},
   280  			exp: "3.14",
   281  		},
   282  		"DecimalVal": {
   283  			update: &pb.Update{Val: &pb.TypedValue{
   284  				Value: &pb.TypedValue_DecimalVal{
   285  					DecimalVal: &pb.Decimal64{Digits: 3014, Precision: 3},
   286  				}}},
   287  			exp: "3.014",
   288  		},
   289  		"DecimalValWithLeadingZeros": {
   290  			update: &pb.Update{Val: &pb.TypedValue{
   291  				Value: &pb.TypedValue_DecimalVal{
   292  					DecimalVal: &pb.Decimal64{Digits: 314, Precision: 6},
   293  				}}},
   294  			exp: "0.000314",
   295  		},
   296  		"DecimalValWithLeadingZerosInFraction": {
   297  			update: &pb.Update{Val: &pb.TypedValue{
   298  				Value: &pb.TypedValue_DecimalVal{
   299  					DecimalVal: &pb.Decimal64{Digits: 3000141, Precision: 6},
   300  				}}},
   301  			exp: "3.000141",
   302  		},
   303  		"DecimalValWithZeroPrecision": {
   304  			update: &pb.Update{Val: &pb.TypedValue{
   305  				Value: &pb.TypedValue_DecimalVal{
   306  					DecimalVal: &pb.Decimal64{Digits: 314, Precision: 0},
   307  				}}},
   308  			exp: "314.0",
   309  		},
   310  		"DecimalValWithNegativeFraction": {
   311  			update: &pb.Update{Val: &pb.TypedValue{
   312  				Value: &pb.TypedValue_DecimalVal{
   313  					DecimalVal: &pb.Decimal64{Digits: -314, Precision: 3},
   314  				}}},
   315  			exp: "-0.314",
   316  		},
   317  		"LeafListVal": {
   318  			update: &pb.Update{Val: &pb.TypedValue{
   319  				Value: &pb.TypedValue_LeaflistVal{
   320  					LeaflistVal: &pb.ScalarArray{Element: []*pb.TypedValue{
   321  						&pb.TypedValue{Value: &pb.TypedValue_BoolVal{BoolVal: true}},
   322  						&pb.TypedValue{Value: &pb.TypedValue_AsciiVal{AsciiVal: "foobar"}},
   323  					}},
   324  				}}},
   325  			exp: "[true, foobar]",
   326  		},
   327  		"AnyVal": {
   328  			update: &pb.Update{Val: &pb.TypedValue{
   329  				Value: &pb.TypedValue_AnyVal{AnyVal: anyMessage}}},
   330  			exp: anyMessage.String(),
   331  		},
   332  		"JsonVal": {
   333  			update: &pb.Update{Val: &pb.TypedValue{
   334  				Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar"}`)}}},
   335  			exp: `{"foo":"bar"}`,
   336  		},
   337  		"JsonVal_complex": {
   338  			update: &pb.Update{Val: &pb.TypedValue{
   339  				Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar","baz":"qux"}`)}}},
   340  			exp: `{
   341    "foo": "bar",
   342    "baz": "qux"
   343  }`,
   344  		},
   345  		"JsonIetfVal": {
   346  			update: &pb.Update{Val: &pb.TypedValue{
   347  				Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{"foo":"bar"}`)}}},
   348  			exp: `{"foo":"bar"}`,
   349  		},
   350  		"AsciiVal": {
   351  			update: &pb.Update{Val: &pb.TypedValue{
   352  				Value: &pb.TypedValue_AsciiVal{AsciiVal: "foobar"}}},
   353  			exp: "foobar",
   354  		},
   355  		"ProtoBytes": {
   356  			update: &pb.Update{Val: &pb.TypedValue{
   357  				Value: &pb.TypedValue_ProtoBytes{ProtoBytes: anyBytes}}},
   358  			exp: "CgZmb29iYXI=",
   359  		},
   360  	} {
   361  		t.Run(name, func(t *testing.T) {
   362  			got := StrUpdateVal(tc.update)
   363  			if got != tc.exp {
   364  				t.Errorf("Expected: %q Got: %q", tc.exp, got)
   365  			}
   366  		})
   367  	}
   368  }
   369  
   370  func TestTypedValue(t *testing.T) {
   371  	for tname, tcase := range map[string]struct {
   372  		in  interface{}
   373  		exp *pb.TypedValue
   374  	}{
   375  		"string": {
   376  			in:  "foo",
   377  			exp: &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}},
   378  		},
   379  		"int": {
   380  			in:  42,
   381  			exp: &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 42}},
   382  		},
   383  		"int64": {
   384  			in:  int64(42),
   385  			exp: &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 42}},
   386  		},
   387  		"uint": {
   388  			in:  uint(42),
   389  			exp: &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: 42}},
   390  		},
   391  		"float32": {
   392  			in:  float32(42.234123),
   393  			exp: &pb.TypedValue{Value: &pb.TypedValue_FloatVal{FloatVal: 42.234123}},
   394  		},
   395  		"float64": {
   396  			in:  float64(42.234124222222),
   397  			exp: &pb.TypedValue{Value: &pb.TypedValue_DoubleVal{DoubleVal: 42.234124222222}},
   398  		},
   399  		"bool": {
   400  			in:  true,
   401  			exp: &pb.TypedValue{Value: &pb.TypedValue_BoolVal{BoolVal: true}},
   402  		},
   403  		"slice": {
   404  			in: []interface{}{"foo", 1, uint(2), true},
   405  			exp: &pb.TypedValue{Value: &pb.TypedValue_LeaflistVal{LeaflistVal: &pb.ScalarArray{
   406  				Element: []*pb.TypedValue{
   407  					&pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}},
   408  					&pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 1}},
   409  					&pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: 2}},
   410  					&pb.TypedValue{Value: &pb.TypedValue_BoolVal{BoolVal: true}},
   411  				}}}},
   412  		},
   413  		"bytes": {
   414  			in:  []byte("foo"),
   415  			exp: &pb.TypedValue{Value: &pb.TypedValue_BytesVal{BytesVal: []byte("foo")}},
   416  		},
   417  		"typed val": {
   418  			in:  &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}},
   419  			exp: &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}},
   420  		},
   421  	} {
   422  		t.Run(tname, func(t *testing.T) {
   423  			if got := TypedValue(tcase.in); !test.DeepEqual(got, tcase.exp) {
   424  				t.Errorf("Expected: %q Got: %q", tcase.exp, got)
   425  			}
   426  		})
   427  	}
   428  }
   429  
   430  func TestExtractJSON(t *testing.T) {
   431  	jsonFile, err := ioutil.TempFile("", "extractContent")
   432  	if err != nil {
   433  		t.Fatal(err)
   434  	}
   435  	defer os.Remove(jsonFile.Name())
   436  	if _, err := jsonFile.Write([]byte(`"jsonFile"`)); err != nil {
   437  		jsonFile.Close()
   438  		t.Fatal(err)
   439  	}
   440  	if err := jsonFile.Close(); err != nil {
   441  		t.Fatal(err)
   442  	}
   443  
   444  	for val, exp := range map[string][]byte{
   445  		jsonFile.Name(): []byte(`"jsonFile"`),
   446  		"foobar":        []byte(`"foobar"`),
   447  		`"foobar"`:      []byte(`"foobar"`),
   448  		"Val: true":     []byte(`"Val: true"`),
   449  		"host42":        []byte(`"host42"`),
   450  		"42":            []byte("42"),
   451  		"-123.43":       []byte("-123.43"),
   452  		"0xFFFF":        []byte("0xFFFF"),
   453  		// Int larger than can fit in 32 bits should be quoted
   454  		"0x8000000000":  []byte(`"0x8000000000"`),
   455  		"-0x8000000000": []byte(`"-0x8000000000"`),
   456  		"true":          []byte("true"),
   457  		"false":         []byte("false"),
   458  		"null":          []byte("null"),
   459  		"{true: 42}":    []byte("{true: 42}"),
   460  		"[]":            []byte("[]"),
   461  	} {
   462  		t.Run(val, func(t *testing.T) {
   463  			got := extractContent(val, "")
   464  			if !bytes.Equal(exp, got) {
   465  				t.Errorf("Unexpected diff. Expected: %q Got: %q", exp, got)
   466  			}
   467  		})
   468  	}
   469  }
   470  
   471  func TestExtractValue(t *testing.T) {
   472  	cases := []struct {
   473  		in  *pb.Update
   474  		exp interface{}
   475  	}{{
   476  		in: &pb.Update{Val: &pb.TypedValue{
   477  			Value: &pb.TypedValue_StringVal{StringVal: "foo"}}},
   478  		exp: "foo",
   479  	}, {
   480  		in: &pb.Update{Val: &pb.TypedValue{
   481  			Value: &pb.TypedValue_IntVal{IntVal: 123}}},
   482  		exp: int64(123),
   483  	}, {
   484  		in: &pb.Update{Val: &pb.TypedValue{
   485  			Value: &pb.TypedValue_UintVal{UintVal: 123}}},
   486  		exp: uint64(123),
   487  	}, {
   488  		in: &pb.Update{Val: &pb.TypedValue{
   489  			Value: &pb.TypedValue_BoolVal{BoolVal: true}}},
   490  		exp: true,
   491  	}, {
   492  		in: &pb.Update{Val: &pb.TypedValue{
   493  			Value: &pb.TypedValue_BytesVal{BytesVal: []byte{0xde, 0xad}}}},
   494  		exp: []byte{0xde, 0xad},
   495  	}, {
   496  		in: &pb.Update{Val: &pb.TypedValue{
   497  			Value: &pb.TypedValue_FloatVal{FloatVal: -12.34}}},
   498  		exp: float32(-12.34),
   499  	}, {
   500  		in: &pb.Update{Val: &pb.TypedValue{
   501  			Value: &pb.TypedValue_DoubleVal{DoubleVal: -12.34}}},
   502  		exp: float64(-12.34),
   503  	}, {
   504  		in: &pb.Update{Val: &pb.TypedValue{
   505  			Value: &pb.TypedValue_DecimalVal{DecimalVal: &pb.Decimal64{
   506  				Digits: -1234, Precision: 2}}}},
   507  		exp: &pb.Decimal64{Digits: -1234, Precision: 2},
   508  	}, {
   509  		in: &pb.Update{Val: &pb.TypedValue{
   510  			Value: &pb.TypedValue_LeaflistVal{LeaflistVal: &pb.ScalarArray{
   511  				Element: []*pb.TypedValue{
   512  					&pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}},
   513  					&pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 123}}}}}}},
   514  		exp: []interface{}{"foo", int64(123)},
   515  	}, {
   516  		in: &pb.Update{Val: &pb.TypedValue{
   517  			Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`12.34`)}}},
   518  		exp: json.Number("12.34"),
   519  	}, {
   520  		in: &pb.Update{Val: &pb.TypedValue{
   521  			Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`[12.34, 123, "foo"]`)}}},
   522  		exp: []interface{}{json.Number("12.34"), json.Number("123"), "foo"},
   523  	}, {
   524  		in: &pb.Update{Val: &pb.TypedValue{
   525  			Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar"}`)}}},
   526  		exp: map[string]interface{}{"foo": "bar"},
   527  	}, {
   528  		in: &pb.Update{Val: &pb.TypedValue{
   529  			Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":45.67}`)}}},
   530  		exp: map[string]interface{}{"foo": json.Number("45.67")},
   531  	}, {
   532  		in: &pb.Update{Val: &pb.TypedValue{
   533  			Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{"foo":"bar"}`)}}},
   534  		exp: map[string]interface{}{"foo": "bar"},
   535  	}}
   536  	for _, tc := range cases {
   537  		out, err := ExtractValue(tc.in)
   538  		if err != nil {
   539  			t.Errorf(err.Error())
   540  		}
   541  		if !test.DeepEqual(tc.exp, out) {
   542  			t.Errorf("Extracted value is incorrect. Expected %+v, got %+v", tc.exp, out)
   543  		}
   544  	}
   545  }