memos/server/router/api/v1/gateway_marshaler_test.go

91 lines
3.1 KiB
Go

package v1
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
)
// TestGatewayMarshalerOmitsUnsetMessageFields pins the REST payload shape the
// generated OpenAPI schema describes. grpc-gateway's stock marshaler writes
// `"motionMedia": null` for a plain image attachment, which no schema declares
// as nullable, and clients that validate against the spec — including strict
// MCP clients, whose tool outputSchema is that same schema — reject the
// response outright.
func TestGatewayMarshalerOmitsUnsetMessageFields(t *testing.T) {
attachment := &v1pb.Attachment{
Name: "attachments/plainimage1",
CreateTime: timestamppb.New(time.Unix(1700000000, 0).UTC()),
Filename: "sunset.png",
Type: "image/png",
}
payload := marshalThroughGateway(t, attachment)
require.NotContains(t, payload, "motionMedia", "an unset message field must be omitted, not emitted as null")
// Scalar defaults stay in the payload: the schema lists filename and type as
// required, so dropping unpopulated scalars would break validation instead.
require.Equal(t, "", payload["externalLink"])
require.Equal(t, "sunset.png", payload["filename"])
require.Equal(t, "image/png", payload["type"])
require.Equal(t, "0", payload["size"])
}
func TestGatewayMarshalerKeepsPopulatedMessageFields(t *testing.T) {
attachment := &v1pb.Attachment{
Name: "attachments/livephoto1",
Filename: "walk.heic",
Type: "image/heic",
MotionMedia: &v1pb.MotionMedia{
Family: v1pb.MotionMediaFamily_APPLE_LIVE_PHOTO,
Role: v1pb.MotionMediaRole_STILL,
GroupId: "group1",
},
}
payload := marshalThroughGateway(t, attachment)
motionMedia, ok := payload["motionMedia"].(map[string]any)
require.True(t, ok, "a populated message field must still be emitted: %v", payload["motionMedia"])
require.Equal(t, "APPLE_LIVE_PHOTO", motionMedia["family"])
require.Equal(t, "group1", motionMedia["groupId"])
}
// TestGatewayMarshalerKeepsEmptyCollections guards the other half of the
// default-value behaviour: list fields the schema types as arrays must not
// disappear when empty.
func TestGatewayMarshalerKeepsEmptyCollections(t *testing.T) {
memo := &v1pb.Memo{
Name: "memos/plainmemo1",
Content: "",
State: v1pb.State_NORMAL,
Visibility: v1pb.Visibility_PRIVATE,
}
payload := marshalThroughGateway(t, memo)
require.Equal(t, []any{}, payload["attachments"])
require.Equal(t, []any{}, payload["relations"])
require.Equal(t, "", payload["content"])
require.Equal(t, false, payload["pinned"])
// location is `optional` in the proto, so it is already omitted rather than
// null; property has no such marker and would otherwise be null here.
require.NotContains(t, payload, "location")
require.NotContains(t, payload, "property")
}
func marshalThroughGateway(t *testing.T, message any) map[string]any {
t.Helper()
data, err := newGatewayMarshaler().Marshal(message)
require.NoError(t, err)
payload := map[string]any{}
require.NoError(t, json.Unmarshal(data, &payload))
return payload
}