package xmlrpc
import (
"bytes"
"reflect"
"strings"
"testing"
)
// Python 2.7 xmlrpclib.dumps(..., methodname=...) output, synthetic values.
const authorizeCall = `
test.authorizeProducts2
auths
userId
productId
123
456
use_extracted_assets_if_available
1
max_supported_format
9
`
// PHP-style response: declaration with encoding, indentation, i4, bare text.
const phpResponse = `
1001
Spaced & <name>
0
-7
bare text
1
9007199254740993
0.1
20261029T12:34:56
aGVs
bG8=
`
const faultResponse = `
faultCode4
faultStringToo many parameters.
`
const unicodeCall = "\ntest.echo" +
"café 日本 \U0001F600 ☺" +
""
func TestParseCall(t *testing.T) {
c, err := ParseCall([]byte(authorizeCall))
if err != nil {
t.Fatal(err)
}
want := &Call{Method: "test.authorizeProducts2", Params: []Value{Struct{
{"auths", []Value{[]Value{"userId", "productId"}, []Value{int64(123), int64(456)}}},
{"use_extracted_assets_if_available", true},
{"max_supported_format", int64(9)},
}}}
if !reflect.DeepEqual(c, want) {
t.Fatalf("got %#v", c)
}
roundTripCall(t, c)
}
func TestParseResponse(t *testing.T) {
r, err := ParseResponse([]byte(phpResponse))
if err != nil {
t.Fatal(err)
}
want := &Response{Params: []Value{[]Value{
[]Value{int64(1001), " Spaced & ", false},
[]Value{int64(-7), "bare text", true, int64(9007199254740993), 0.1,
DateTime("20261029T12:34:56"), []byte("hello"), nil, "", "", "", Struct{}, []Value{}},
}}}
if !reflect.DeepEqual(r, want) {
t.Fatalf("got %#v", r)
}
roundTripResponse(t, r)
}
func TestFault(t *testing.T) {
r, err := ParseResponse([]byte(faultResponse))
if err != nil {
t.Fatal(err)
}
if r.Fault == nil || *r.Fault != (Fault{4, "Too many parameters."}) || r.Params != nil {
t.Fatalf("got %#v", r)
}
out := r.Marshal()
if !bytes.Contains(out, []byte("\n")) {
t.Fatalf("marshal: %s", out)
}
roundTripResponse(t, r)
}
func TestUnicode(t *testing.T) {
c, err := ParseCall([]byte(unicodeCall))
if err != nil {
t.Fatal(err)
}
if s := c.Params[0]; s != "café 日本 \U0001F600 ☺" {
t.Fatalf("got %q", s)
}
roundTripCall(t, c)
}
func TestWhitespaceInString(t *testing.T) {
r := &Response{Params: []Value{" a\tb\r\nc ", Struct{{"k\n", "\r"}}, 1.5e300, -0.0}}
roundTripResponse(t, r)
}
func TestMarshalFormat(t *testing.T) {
c := &Call{Method: "m", Params: []Value{int64(1), true, "a<&>", nil, 2.5}}
want := "\n\nm\n\n" +
"\n1\n\n" +
"\n1\n\n" +
"\na<&>\n\n" +
"\n\n\n" +
"\n2.5\n\n" +
"\n\n"
if got := string(c.Marshal()); got != want {
t.Fatalf("got\n%s", got)
}
}
func TestCallWithoutParams(t *testing.T) {
c, err := ParseCall([]byte("x"))
if err != nil || c.Method != "x" || len(c.Params) != 0 {
t.Fatal(c, err)
}
}
func TestErrors(t *testing.T) {
bad := []string{
"",
"",
"x",
"2",
"z",
"!!",
"",
"x1",
"1",
"1",
"a",
"",
"xjunk",
`x`,
}
for _, s := range bad {
_, err1 := ParseResponse([]byte(s))
_, err2 := ParseCall([]byte(s))
if err1 == nil && err2 == nil {
t.Errorf("accepted %q", s)
}
}
}
func nested(n int) string {
return "" +
strings.Repeat("", n) + "1" +
strings.Repeat("", n) +
""
}
func TestDepth(t *testing.T) {
if _, err := ParseResponse([]byte(nested(MaxDepth))); err != nil {
t.Fatal(err)
}
if _, err := ParseResponse([]byte(nested(MaxDepth + 1))); err == nil {
t.Fatal("accepted too deep")
}
}
func TestSize(t *testing.T) {
big := make([]byte, MaxSize+1)
if _, err := ParseCall(big); err == nil {
t.Fatal("accepted too large")
}
}
func roundTripCall(t *testing.T, c *Call) {
t.Helper()
c2, err := ParseCall(c.Marshal())
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(c, c2) {
t.Fatalf("round trip:\n%#v\n%#v", c, c2)
}
}
func roundTripResponse(t *testing.T, r *Response) {
t.Helper()
r2, err := ParseResponse(r.Marshal())
if err != nil {
t.Fatal(err)
}
if r.Params == nil && r.Fault == nil {
r.Params = []Value{}
}
if !reflect.DeepEqual(r, r2) {
t.Fatalf("round trip:\n%#v\n%#v", r, r2)
}
}
// Fuzzed documents must not panic, and whatever parses must re-marshal stably
// (bytes, not values, so NaN compares equal).
func FuzzParseCall(f *testing.F) {
f.Add([]byte(authorizeCall))
f.Add([]byte(unicodeCall))
f.Fuzz(func(t *testing.T, data []byte) {
c, err := ParseCall(data)
if err != nil {
return
}
out := c.Marshal()
c2, err := ParseCall(out)
if err != nil {
t.Fatalf("reparse: %v\n%s", err, out)
}
if !bytes.Equal(out, c2.Marshal()) {
t.Fatalf("unstable:\n%s", out)
}
})
}
func FuzzParseResponse(f *testing.F) {
f.Add([]byte(phpResponse))
f.Add([]byte(faultResponse))
f.Add([]byte(nested(3)))
f.Fuzz(func(t *testing.T, data []byte) {
r, err := ParseResponse(data)
if err != nil {
return
}
out := r.Marshal()
r2, err := ParseResponse(out)
if err != nil {
t.Fatalf("reparse: %v\n%s", err, out)
}
if !bytes.Equal(out, r2.Marshal()) {
t.Fatalf("unstable:\n%s", out)
}
})
}