From 80d6046e6199aac1f1f4b0a634263d16337c1c07 Mon Sep 17 00:00:00 2001 From: Akash Patel Date: Mon, 24 Feb 2025 23:35:09 +0530 Subject: [PATCH 1/2] added marshaller and unit test cases --- runtime/marshal_urlencode.go | 62 ++++++++++++ runtime/marshal_urlencode_test.go | 150 ++++++++++++++++++++++++++++++ runtime/marshaler_registry.go | 30 ++++-- 3 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 runtime/marshal_urlencode.go create mode 100644 runtime/marshal_urlencode_test.go diff --git a/runtime/marshal_urlencode.go b/runtime/marshal_urlencode.go new file mode 100644 index 00000000000..c8a9c8bbc38 --- /dev/null +++ b/runtime/marshal_urlencode.go @@ -0,0 +1,62 @@ +package runtime + +import ( + "fmt" + "io" + "net/url" + + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/protobuf/proto" +) + +type UrlEncodedDecoder struct { + r io.Reader +} + +func NewUrlEncodedDecoder(r io.Reader) Decoder { + return &UrlEncodedDecoder{r: r} +} + +func (u *UrlEncodedDecoder) Decode(v interface{}) error { + msg, ok := v.(proto.Message) + if !ok { + return fmt.Errorf("not proto message") + } + + formData, err := io.ReadAll(u.r) + if err != nil { + return err + } + + values, err := url.ParseQuery(string(formData)) + if err != nil { + return err + } + + filter := &utilities.DoubleArray{} + + err = PopulateQueryParameters(msg, values, filter) + if err != nil { + return err + } + + return nil +} + +type UrlEncodeMarshal struct { + Marshaler +} + +// ContentType means the content type of the response +func (u *UrlEncodeMarshal) ContentType(_ interface{}) string { + return "application/json" +} + +func (u *UrlEncodeMarshal) Marshal(v interface{}) ([]byte, error) { + return u.Marshaler.Marshal(v) +} + +// NewDecoder indicates how to decode the request +func (u UrlEncodeMarshal) NewDecoder(r io.Reader) Decoder { + return NewUrlEncodedDecoder(r) +} diff --git a/runtime/marshal_urlencode_test.go b/runtime/marshal_urlencode_test.go new file mode 100644 index 00000000000..8c86b861de6 --- /dev/null +++ b/runtime/marshal_urlencode_test.go @@ -0,0 +1,150 @@ +package runtime + +import ( + "bytes" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime/internal/examplepb" + "google.golang.org/protobuf/proto" +) + +func TestUrlEncodedDecoder_Decode(t *testing.T) { + tests := []struct { + name string + values url.Values + want proto.Message + wantErr bool + }{ + { + name: "simple form fields", + values: url.Values{ + "single_nested.name": {"test"}, + "single_nested.amount": {"42"}, + }, + want: &examplepb.ABitOfEverything{ + SingleNested: &examplepb.ABitOfEverything_Nested{ + Name: "test", + Amount: 42, + }, + }, + wantErr: false, + }, + { + name: "fields with special characters", + values: url.Values{ + "single_nested.name": {"Hello World!"}, + "single_nested.amount": {"123"}, + }, + want: &examplepb.ABitOfEverything{ + SingleNested: &examplepb.ABitOfEverything_Nested{ + Name: "Hello World!", + Amount: 123, + }, + }, + wantErr: false, + }, + { + name: "empty input", + values: url.Values{}, + want: &examplepb.ABitOfEverything{}, + wantErr: false, + }, + { + name: "repeated field", + values: url.Values{ + "repeated_string": {"one", "two", "three"}, + }, + want: &examplepb.ABitOfEverything{ + RepeatedStringValue: []string{"one", "two", "three"}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader(tt.values.Encode())) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + decoder := NewUrlEncodedDecoder(req.Body) + msg := &examplepb.ABitOfEverything{} + + err = decoder.Decode(msg) + if (err != nil) != tt.wantErr { + t.Errorf("UrlEncodedDecoder.Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && !proto.Equal(msg, tt.want) { + t.Errorf("UrlEncodedDecoder.Decode() = %v, want %v", msg, tt.want) + } + }) + } +} + +func TestUrlEncodedDecoder_DecodeNonProto(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader("")) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + decoder := NewUrlEncodedDecoder(req.Body) + var nonProto struct{} + + err = decoder.Decode(&nonProto) + if err == nil { + t.Error("UrlEncodedDecoder.Decode() expected error for non-proto message") + } +} + +func TestUrlEncodeMarshal_ContentType(t *testing.T) { + m := &UrlEncodeMarshal{} + if got := m.ContentType(nil); got != "application/x-www-form-urlencoded" { + t.Errorf("UrlEncodeMarshal.ContentType() = %v, want application/x-www-form-urlencoded", got) + } +} + +func TestUrlEncodeMarshal_Marshal(t *testing.T) { + msg := &examplepb.ABitOfEverything{ + SingleNested: &examplepb.ABitOfEverything_Nested{ + Name: "test", + Amount: 42, + }, + } + + marshaler := &UrlEncodeMarshal{ + Marshaler: &JSONPb{}, + } + + got, err := marshaler.Marshal(msg) + if err != nil { + t.Fatalf("UrlEncodeMarshal.Marshal() error = %v", err) + } + + want := []byte(`{"single_nested":{"name":"test","amount":42}}`) + if !bytes.Equal(got, want) { + t.Errorf("UrlEncodeMarshal.Marshal() = %s, want %s", got, want) + } +} + +func TestUrlEncodeMarshal_NewDecoder(t *testing.T) { + m := &UrlEncodeMarshal{} + req, err := http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader("")) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + decoder := m.NewDecoder(req.Body) + + if _, ok := decoder.(*UrlEncodedDecoder); !ok { + t.Error("UrlEncodeMarshal.NewDecoder() did not return *UrlEncodedDecoder") + } +} diff --git a/runtime/marshaler_registry.go b/runtime/marshaler_registry.go index 07c28112c89..68b7bc54623 100644 --- a/runtime/marshaler_registry.go +++ b/runtime/marshaler_registry.go @@ -11,21 +11,30 @@ import ( // MIMEWildcard is the fallback MIME type used for requests which do not match // a registered MIME type. -const MIMEWildcard = "*" +const ( + MIMEWildcard = "*" + MIMEUrlEncoded = "application/x-www-form-urlencoded" +) var ( acceptHeader = http.CanonicalHeaderKey("Accept") contentTypeHeader = http.CanonicalHeaderKey("Content-Type") - defaultMarshaler = &HTTPBodyMarshaler{ - Marshaler: &JSONPb{ - MarshalOptions: protojson.MarshalOptions{ - EmitUnpopulated: true, - }, - UnmarshalOptions: protojson.UnmarshalOptions{ - DiscardUnknown: true, - }, + defaultJsonPbMarshaler = &JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + EmitUnpopulated: true, }, + UnmarshalOptions: protojson.UnmarshalOptions{ + DiscardUnknown: true, + }, + } + + defaultMarshaler = &HTTPBodyMarshaler{ + Marshaler: defaultJsonPbMarshaler, + } + + urlEncodedMarshaler = &UrlEncodeMarshal{ + Marshaler: defaultJsonPbMarshaler, } ) @@ -93,7 +102,8 @@ func (m marshalerRegistry) add(mime string, marshaler Marshaler) error { func makeMarshalerMIMERegistry() marshalerRegistry { return marshalerRegistry{ mimeMap: map[string]Marshaler{ - MIMEWildcard: defaultMarshaler, + MIMEWildcard: defaultMarshaler, + MIMEUrlEncoded: urlEncodedMarshaler, }, } } From d6e8fc452a2078efa64b1a328479017a59cbd336 Mon Sep 17 00:00:00 2001 From: Akash Patel Date: Mon, 24 Feb 2025 23:51:18 +0530 Subject: [PATCH 2/2] fix test cases --- runtime/marshal_urlencode.go | 2 +- runtime/marshal_urlencode_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime/marshal_urlencode.go b/runtime/marshal_urlencode.go index c8a9c8bbc38..8de188a4cc9 100644 --- a/runtime/marshal_urlencode.go +++ b/runtime/marshal_urlencode.go @@ -57,6 +57,6 @@ func (u *UrlEncodeMarshal) Marshal(v interface{}) ([]byte, error) { } // NewDecoder indicates how to decode the request -func (u UrlEncodeMarshal) NewDecoder(r io.Reader) Decoder { +func (u *UrlEncodeMarshal) NewDecoder(r io.Reader) Decoder { return NewUrlEncodedDecoder(r) } diff --git a/runtime/marshal_urlencode_test.go b/runtime/marshal_urlencode_test.go index 8c86b861de6..4aaeeed6c36 100644 --- a/runtime/marshal_urlencode_test.go +++ b/runtime/marshal_urlencode_test.go @@ -55,7 +55,7 @@ func TestUrlEncodedDecoder_Decode(t *testing.T) { { name: "repeated field", values: url.Values{ - "repeated_string": {"one", "two", "three"}, + "repeated_string_value": {"one", "two", "three"}, }, want: &examplepb.ABitOfEverything{ RepeatedStringValue: []string{"one", "two", "three"}, @@ -106,8 +106,8 @@ func TestUrlEncodedDecoder_DecodeNonProto(t *testing.T) { func TestUrlEncodeMarshal_ContentType(t *testing.T) { m := &UrlEncodeMarshal{} - if got := m.ContentType(nil); got != "application/x-www-form-urlencoded" { - t.Errorf("UrlEncodeMarshal.ContentType() = %v, want application/x-www-form-urlencoded", got) + if got := m.ContentType(nil); got != "application/json" { + t.Errorf("UrlEncodeMarshal.ContentType() = %v, want application/json", got) } }