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) } }) }