From cbc9bc45faa4a6842e2d54120099d88392685d92 Mon Sep 17 00:00:00 2001 From: Brandon Bennett Date: Mon, 10 Apr 2023 14:45:02 -0600 Subject: [PATCH] improve `RPCMsg` xml encoding testing (#36) Add unittest for RPCMsg encoding and ensure that operation is supplied when encoding `rpc` request messages (i.e `ReqMSG`) via custom MarshalXML method. Future work to make sure structs passed in are named. **Test Plan:** ``` $ go test . -v -run=MarshalRPCMsg === RUN TestMarshalRPCMsg === RUN TestMarshalRPCMsg/nil msg_test.go:55: out: === RUN TestMarshalRPCMsg/string msg_test.go:55: out: === RUN TestMarshalRPCMsg/byteslice msg_test.go:55: out: === RUN TestMarshalRPCMsg/validate msg_test.go:55: out: === RUN TestMarshalRPCMsg/customStruct msg_test.go:55: out: show bgp neighbors --- PASS: TestMarshalRPCMsg (0.00s) --- PASS: TestMarshalRPCMsg/nil (0.00s) --- PASS: TestMarshalRPCMsg/string (0.00s) --- PASS: TestMarshalRPCMsg/byteslice (0.00s) --- PASS: TestMarshalRPCMsg/validate (0.00s) --- PASS: TestMarshalRPCMsg/customStruct (0.00s) PASS ok github.com/nemith/netconf (cached) ``` --- .vscode/settings.json | 9 +++++- inttest/ssh_test.go | 30 +++++++++++++++++ msg.go | 13 ++++++++ msg_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 msg_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 14c5890..d1c735a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,10 @@ { - "cSpell.words": ["netconf"] + "cSpell.words": [ + "byteslice", + "innerxml", + "inttest", + "Junos", + "nemith", + "netconf" + ] } diff --git a/inttest/ssh_test.go b/inttest/ssh_test.go index 1bfe0df..b84f9ec 100644 --- a/inttest/ssh_test.go +++ b/inttest/ssh_test.go @@ -5,8 +5,10 @@ package inttest import ( "context" + "encoding/xml" "net" "os" + "strings" "testing" "github.com/nemith/netconf" @@ -15,7 +17,19 @@ import ( "golang.org/x/crypto/ssh" ) +func onlyFlavor(t *testing.T, flavors ...string) { + t.Helper() + for _, flavor := range flavors { + if os.Getenv("NETCONF_DUT_FLAVOR") == flavor { + return + } + } + t.Skipf("test only for flavors '%s'. Skipping", strings.Join(flavors, ",")) +} + func sshAuth(t *testing.T) ssh.AuthMethod { + t.Helper() + switch { case os.Getenv("NETCONF_DUT_SSHPASS") != "": return ssh.Password(os.Getenv("NETCONF_DUT_SSHPASS")) @@ -117,3 +131,19 @@ func TestBadGetConfig(t *testing.T) { assert.ErrorAs(t, err, &rpcErrors) assert.Len(t, rpcErrors, 1) } + +func TestJunosCommand(t *testing.T) { + onlyFlavor(t, "junos") + session := setupSSH(t) + + cmd := struct { + XMLName xml.Name `xml:"command"` + Command string `xml:",innerxml"` + }{ + Command: "show version", + } + + ctx := context.Background() + err := session.Call(ctx, &cmd, nil) + assert.NoError(t, err) +} diff --git a/msg.go b/msg.go index df2e731..6d6e4dd 100644 --- a/msg.go +++ b/msg.go @@ -21,6 +21,19 @@ type RPCMsg struct { Operation interface{} `xml:",innerxml"` } +func (msg *RPCMsg) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if msg.Operation == nil { + return fmt.Errorf("operation cannot be nil") + } + + // TODO: validate operation is named? + + // alias the type to not cause recursion calling e.Encode + type rpcMsg RPCMsg + inner := rpcMsg(*msg) + return e.Encode(&inner) +} + // RPCReplyMsg maps the xml value of in RFC6241 type RPCReplyMsg struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 rpc-reply"` diff --git a/msg_test.go b/msg_test.go new file mode 100644 index 0000000..a7f73c5 --- /dev/null +++ b/msg_test.go @@ -0,0 +1,75 @@ +package netconf + +import ( + "encoding/xml" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMarshalRPCMsg(t *testing.T) { + tt := []struct { + name string + operation interface{} + err bool + want []byte + }{ + { + name: "nil", + operation: nil, + err: true, + }, + { + name: "string", + operation: "", + want: []byte(``), + }, + { + name: "byteslice", + operation: []byte(""), + want: []byte(``), + }, + { + name: "validate", + operation: validateReq{Source: Running}, + want: []byte(``), + }, + { + name: "namedStruct", + operation: struct { + XMLName xml.Name `xml:"http://xml.juniper.net/junos/22.4R0/junos command"` + Command string `xml:",innerxml"` + }{ + Command: "show bgp neighbors", + }, + want: []byte(`show bgp neighbors`), + }, + /* + { + name: "unnamedStruct", + operation: struct { + Command string `xml:"command"` + }{ + Command: "show version", + }, + },*/ + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + out, err := xml.Marshal(&RPCMsg{ + MessageID: 1, + Operation: tc.operation, + }) + t.Logf("out: %s", out) + + if tc.err { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, out, tc.want) + }) + } +}