| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- package wire
- import (
- "testing"
- "time"
- goAMF3 "github.com/breign/goAMF3"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // marshalAMF3Map encodes v and decodes it back as an AMF3 object.
- func marshalAMF3Map(t *testing.T, v any) map[string]any {
- t.Helper()
- b, err := MarshalAMF3(v)
- require.NoError(t, err)
- decoded := goAMF3.DecodeAMF3(b)
- m, ok := decoded.(map[string]any)
- require.True(t, ok, "decoded as %T, want an object", decoded)
- return m
- }
- // amf3EventType stands in for the named string types that carry an event's type
- // constant.
- type amf3EventType string
- type amf3Tagged struct {
- Sender string `json:"sender" amf3:"source"`
- AutoResp bool `json:"autoResponse,omitempty" amf3:"autoresponse"`
- Message string `json:"message"`
- State string `json:"state,omitempty" amf3:"state"`
- Secret string `json:"-"`
- Hidden string `amf3:"-" json:"hidden"`
- MsgID string `json:"msgId,omitempty"`
- unseen string
- }
- // An amf3 tag replaces the json tag, which is how the same struct spells a field
- // one way for the Web AIM client and another for the documented JSON API.
- func TestMarshalAMF3TagOverridesJSON(t *testing.T) {
- m := marshalAMF3Map(t, amf3Tagged{
- Sender: "chattingchuck", Message: "hi", Secret: "s", Hidden: "h", unseen: "u",
- })
- assert.Equal(t, "chattingchuck", m["source"])
- assert.NotContains(t, m, "sender")
- assert.Equal(t, "hi", m["message"])
- // An amf3 tag carrying no omitempty keeps a field the JSON encoding drops.
- assert.Contains(t, m, "autoresponse")
- assert.Equal(t, false, m["autoresponse"])
- assert.Contains(t, m, "state")
- assert.Equal(t, "", m["state"])
- // "-" suppresses the field whichever tag spells it, and unexported fields
- // never appear.
- assert.NotContains(t, m, "secret")
- assert.NotContains(t, m, "hidden")
- assert.NotContains(t, m, "unseen")
- // A json omitempty still applies when no amf3 tag overrides it.
- assert.NotContains(t, m, "msgId")
- }
- // AMF3 stores whole numbers in 29 bits, so anything wider has to arrive as a
- // double or it is silently truncated on the wire.
- func TestMarshalAMF3IntegerRange(t *testing.T) {
- tests := []struct {
- name string
- value any
- want any
- }{
- {"zero", 0, int32(0)},
- {"negative", -5, int32(-5)},
- {"max int29", maxInt29, int32(maxInt29)},
- {"min int29", minInt29, int32(minInt29)},
- {"one past max int29", int64(maxInt29 + 1), float64(maxInt29 + 1)},
- {"one past min int29", int64(minInt29 - 1), float64(minInt29 - 1)},
- {"unix timestamp", int64(1700000000), float64(1700000000)},
- {"uint64 in range", uint64(42), int32(42)},
- {"uint64 out of range", uint64(1) << 40, float64(uint64(1) << 40)},
- {"uint64 max", uint64(1<<64 - 1), float64(uint64(1<<64 - 1))},
- {"uint32 out of range", uint32(4000000000), float64(4000000000)},
- {"float stays a double", 1.5, 1.5},
- {"seqNum", uint64(7), int32(7)},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- m := marshalAMF3Map(t, map[string]any{"n": tt.value})
- assert.Equal(t, tt.want, m["n"])
- })
- }
- }
- type amf3Embedded struct {
- AimID string `json:"aimId"`
- }
- type amf3WithEmbedded struct {
- amf3Embedded
- State string `json:"state"`
- }
- type amf3Nested struct {
- Source amf3Embedded `json:"source"`
- Ptr *amf3Embedded `json:"ptr,omitempty"`
- Absent *amf3Embedded `json:"absent,omitempty"`
- Required *amf3Embedded `json:"required"`
- Boxed any `json:"boxed"`
- List []amf3Embedded `json:"list"`
- NilList []string `json:"capabilities"`
- Counts map[string]int `json:"counts"`
- Any any `json:"any"`
- When time.Time `json:"when"`
- }
- func TestMarshalAMF3NestedValues(t *testing.T) {
- when := time.Unix(1700000000, 0).UTC()
- m := marshalAMF3Map(t, amf3Nested{
- Source: amf3Embedded{AimID: "chuck"},
- Ptr: &amf3Embedded{AimID: "fred"},
- List: []amf3Embedded{{AimID: "one"}},
- Counts: map[string]int{"unread": 3},
- Any: amf3Embedded{AimID: "boxed"},
- When: when,
- })
- assert.Equal(t, map[string]any{"aimId": "chuck"}, m["source"])
- assert.Equal(t, map[string]any{"aimId": "fred"}, m["ptr"])
- assert.Equal(t, []any{map[string]any{"aimId": "one"}}, m["list"])
- // A map of a concrete value type reaches the wire; the writer takes only
- // map[string]any on its own.
- assert.Equal(t, map[string]any{"unread": int32(3)}, m["counts"])
- assert.Equal(t, map[string]any{"aimId": "boxed"}, m["any"])
- // An AMF3 date is a bare UTC epoch, so the instant survives but the zone
- // does not.
- decodedWhen, ok := m["when"].(time.Time)
- require.True(t, ok, "when decoded as %T", m["when"])
- assert.True(t, when.Equal(decodedWhen), "got %s, want %s", decodedWhen, when)
- // A nil list is sent as an empty one because the client iterates lists such
- // as capabilities unconditionally.
- assert.Equal(t, []any{}, m["capabilities"])
- // omitempty is what drops a nil, so the client's merge leaves whatever it
- // already holds alone.
- assert.NotContains(t, m, "absent")
- // Without omitempty the field is one the client dereferences on sight, so it
- // arrives as an empty object rather than an absent key.
- assert.Equal(t, map[string]any{}, m["required"])
- assert.Equal(t, map[string]any{}, m["boxed"])
- }
- // An untagged embedded struct contributes its fields to the enclosing object.
- func TestMarshalAMF3PromotesEmbeddedFields(t *testing.T) {
- m := marshalAMF3Map(t, amf3WithEmbedded{amf3Embedded: amf3Embedded{AimID: "chuck"}, State: "online"})
- assert.Equal(t, map[string]any{"aimId": "chuck", "state": "online"}, m)
- }
- // A named string encodes as its underlying string, which is what carries an
- // event's type constant.
- func TestMarshalAMF3NamedString(t *testing.T) {
- m := marshalAMF3Map(t, map[string]any{"type": amf3EventType("presence")})
- assert.Equal(t, "presence", m["type"])
- }
- // The AMF3 writer emits nothing for a value it cannot handle, truncating the
- // enclosing object mid-key, so an unsupported type has to be an error instead.
- func TestMarshalAMF3RejectsUnsupportedTypes(t *testing.T) {
- tests := []struct {
- name string
- value any
- }{
- {"channel", make(chan int)},
- {"func", func() {}},
- {"complex", complex(1, 2)},
- {"struct carrying a channel", struct {
- Ch chan int `json:"ch"`
- }{Ch: make(chan int)}},
- {"slice of channels", []chan int{make(chan int)}},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- _, err := MarshalAMF3(tt.value)
- assert.Error(t, err)
- })
- }
- }
- // Every response is an object, so a nil payload is an empty one rather than a
- // null the client would dereference.
- func TestMarshalAMF3NilIsAnEmptyObject(t *testing.T) {
- assert.Equal(t, map[string]any{}, marshalAMF3Map(t, nil))
- assert.Equal(t, map[string]any{}, marshalAMF3Map(t, (*amf3Nested)(nil)))
- assert.Equal(t, map[string]any{}, marshalAMF3Map(t, struct{}{}))
- }
- // Byte slices are the one slice written as an AMF3 byte array.
- func TestMarshalAMF3ByteSlice(t *testing.T) {
- b, err := MarshalAMF3(map[string]any{"raw": []byte{1, 2, 3}})
- require.NoError(t, err)
- m, ok := goAMF3.DecodeAMF3(b).(map[string]any)
- require.True(t, ok)
- assert.Equal(t, []byte{1, 2, 3}, m["raw"])
- }
- // TestMarshalAMF3_OmitZero pins the omitzero option: unlike omitempty it suppresses
- // only the zero value, so an empty-but-non-nil slice still encodes.
- func TestMarshalAMF3_OmitZero(t *testing.T) {
- type payload struct {
- Users []string `json:"users,omitzero"`
- }
- tests := []struct {
- name string
- value payload
- want string
- }{
- {name: "nil is omitted", value: payload{}, want: ""},
- {name: "empty is kept", value: payload{Users: []string{}}, want: "users"},
- {name: "populated is kept", value: payload{Users: []string{"bob"}}, want: "users"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- b, err := MarshalAMF3(tt.value)
- assert.NoError(t, err)
- if tt.want == "" {
- assert.NotContains(t, string(b), "users")
- } else {
- assert.Contains(t, string(b), "users")
- }
- })
- }
- }
|