Skip to content

Commit

Permalink
improve RPCMsg xml encoding testing (#36)
Browse files Browse the repository at this point in the history
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: <rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1"><foo><bar/></foo></rpc>
=== RUN   TestMarshalRPCMsg/byteslice
    msg_test.go:55: out: <rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1"><baz><qux/></baz></rpc>
=== RUN   TestMarshalRPCMsg/validate
    msg_test.go:55: out: <rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1"><validate><source><running/></source></validate></rpc>
=== RUN   TestMarshalRPCMsg/customStruct
    msg_test.go:55: out: <rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1"><command xmlns="http://xml.juniper.net/junos/22.4R0/junos">show bgp neighbors</command></rpc>
--- 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)
```
  • Loading branch information
nemith authored Apr 10, 2023
1 parent 5058b36 commit cbc9bc4
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 1 deletion.
9 changes: 8 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
{
"cSpell.words": ["netconf"]
"cSpell.words": [
"byteslice",
"innerxml",
"inttest",
"Junos",
"nemith",
"netconf"
]
}
30 changes: 30 additions & 0 deletions inttest/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package inttest

import (
"context"
"encoding/xml"
"net"
"os"
"strings"
"testing"

"github.com/nemith/netconf"
Expand All @@ -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"))
Expand Down Expand Up @@ -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)
}
13 changes: 13 additions & 0 deletions msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <rpc-reply> in RFC6241
type RPCReplyMsg struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 rpc-reply"`
Expand Down
75 changes: 75 additions & 0 deletions msg_test.go
Original file line number Diff line number Diff line change
@@ -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: "<foo><bar/></foo>",
want: []byte(`<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1"><foo><bar/></foo></rpc>`),
},
{
name: "byteslice",
operation: []byte("<baz><qux/></baz>"),
want: []byte(`<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1"><baz><qux/></baz></rpc>`),
},
{
name: "validate",
operation: validateReq{Source: Running},
want: []byte(`<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1"><validate><source><running/></source></validate></rpc>`),
},
{
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(`<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1"><command xmlns="http://xml.juniper.net/junos/22.4R0/junos">show bgp neighbors</command></rpc>`),
},
/*
{
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)
})
}
}

0 comments on commit cbc9bc4

Please sign in to comment.