package handlers import ( "fmt" "net/http" "net/http/httptest" "testing" "time" goAMF3 "github.com/breign/goAMF3" "github.com/mk6i/open-oscar-server/server/webapi/types" ) func TestAMFEncoderBasicTypes(t *testing.T) { encoder := NewAMFEncoder(nil) tests := []struct { name string input interface{} version AMFVersion wantErr bool }{ {"String AMF3", "hello world", AMF3, false}, {"Number AMF3", 42, AMF3, false}, {"Float AMF3", 3.14159, AMF3, false}, {"Boolean AMF3", false, AMF3, false}, {"Null AMF3", nil, AMF3, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { data, err := encoder.EncodeAMF(tt.input, tt.version) if (err != nil) != tt.wantErr { t.Fatalf("EncodeAMF() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr && len(data) == 0 { t.Fatal("EncodeAMF() returned empty data") } // Try to decode the data to verify it's valid AMF3 if !tt.wantErr { decoded := goAMF3.DecodeAMF3(data) if decoded == nil { t.Fatalf("Failed to decode AMF3 data: got nil result") } } }) } } func TestAMFEncoderComplexTypes(t *testing.T) { encoder := NewAMFEncoder(nil) tests := []struct { name string input interface{} version AMFVersion }{ { name: "Map", input: map[string]interface{}{ "name": "John Doe", "age": 30, "active": true, }, version: AMF3, }, { name: "Array", input: []interface{}{ "item1", 42, true, nil, }, version: AMF3, }, { name: "BaseResponse", input: BaseResponse{ Response: ResponseBody{ StatusCode: 200, StatusText: "OK", Data: map[string]interface{}{ "user": "testuser", "online": true, "buddies": []interface{}{ "friend1", "friend2", }, }, }, }, version: AMF3, }, { name: "ErrorResponse", input: newErrorResponse(404, "Not Found"), version: AMF3, }, { name: "Time", input: map[string]interface{}{ "timestamp": time.Now(), "name": "Event", }, version: AMF3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { data, err := encoder.EncodeAMF(tt.input, tt.version) if err != nil { t.Fatalf("EncodeAMF() error = %v", err) } if len(data) == 0 { t.Fatal("EncodeAMF() returned empty data") } // Verify the data is valid AMF decoded := goAMF3.DecodeAMF3(data) if decoded == nil { t.Fatalf("Failed to decode AMF data: got nil result") } // Log the size for performance comparison t.Logf("%s: %d bytes", tt.name, len(data)) }) } } func TestDetectAMFVersion(t *testing.T) { tests := []struct { name string request *http.Request expected AMFVersion }{ { name: "Query parameter amf3", request: httptest.NewRequest("GET", "/?f=amf3", nil), expected: AMF3, }, { name: "Query parameter amf", request: httptest.NewRequest("GET", "/?f=amf", nil), expected: AMF3, }, { name: "Accept header AMF3", request: func() *http.Request { req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Accept", "application/x-amf3") return req }(), expected: AMF3, }, { name: "Accept header AMF", request: func() *http.Request { req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Accept", "application/x-amf") return req }(), expected: AMF3, }, { name: "No AMF indication", request: httptest.NewRequest("GET", "/", nil), expected: AMF3, }, { name: "Nil request", request: nil, expected: AMF3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { version := DetectAMFVersion(tt.request) if version != tt.expected { t.Errorf("DetectAMFVersion() = %v, want %v", version, tt.expected) } }) } } func TestSendAMF(t *testing.T) { tests := []struct { name string request *http.Request data interface{} expectStatus int }{ { name: "Simple response", request: httptest.NewRequest("GET", "/?f=amf", nil), data: BaseResponse{ Response: ResponseBody{ StatusCode: 200, StatusText: "OK", Data: map[string]interface{}{"test": "value"}, }, }, expectStatus: http.StatusOK, }, { name: "AMF3 response with array", request: httptest.NewRequest("GET", "/?f=amf3", nil), data: BaseResponse{ Response: ResponseBody{ StatusCode: 200, StatusText: "OK", Data: []interface{}{"item1", "item2"}, }, }, expectStatus: http.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // First test if the encoder can handle the data encoder := NewAMFEncoder(nil) version := DetectAMFVersion(tt.request) _, encodeErr := encoder.EncodeAMF(tt.data, version) if encodeErr != nil { t.Fatalf("Encoding failed: %v", encodeErr) } w := httptest.NewRecorder() sendAMF(w, tt.request, tt.data, nil) resp := w.Result() if resp.StatusCode != tt.expectStatus { t.Errorf("Expected status %d, got %d", tt.expectStatus, resp.StatusCode) // Print response body for debugging body := w.Body.String() t.Logf("Response body: %s", body) } contentType := resp.Header.Get("Content-Type") if contentType != "application/x-amf" { t.Errorf("Expected Content-Type application/x-amf, got %s", contentType) } body := w.Body.Bytes() if len(body) == 0 { t.Error("Response body is empty") } }) } } func TestStructToMap(t *testing.T) { encoder := NewAMFEncoder(nil) type TestStruct struct { Name string `json:"name"` Age int `json:"age"` Active bool `json:"active"` Hidden string `json:"-"` Optional string `json:"optional,omitempty"` NoTag string } testStruct := TestStruct{ Name: "John", Age: 30, Active: true, Hidden: "should not appear", Optional: "", // should be omitted NoTag: "should appear with field name", } result := encoder.toAMF3Compatible(testStruct) resultMap, ok := result.(map[string]interface{}) if !ok { t.Fatal("Expected map[string]interface{}") } // Check expected fields if resultMap["name"] != "John" { t.Errorf("Expected name=John, got %v", resultMap["name"]) } if resultMap["age"] != 30 { t.Errorf("Expected age=30, got %v", resultMap["age"]) } if resultMap["active"] != true { t.Errorf("Expected active=true, got %v", resultMap["active"]) } if resultMap["NoTag"] != "should appear with field name" { t.Errorf("Expected NoTag field, got %v", resultMap["NoTag"]) } // Check omitted fields if _, exists := resultMap["Hidden"]; exists { t.Error("Hidden field should not appear") } if _, exists := resultMap["optional"]; exists { t.Error("Optional empty field should be omitted") } } func TestSliceToArray(t *testing.T) { encoder := NewAMFEncoder(nil) input := []interface{}{ "string", 42, true, nil, map[string]interface{}{"nested": "value"}, } result := encoder.toAMF3Compatible(input) resultArray, ok := result.([]interface{}) if !ok { t.Fatal("Expected []interface{}") } if len(resultArray) != 5 { t.Errorf("Expected 5 elements, got %d", len(resultArray)) } if resultArray[0] != "string" { t.Errorf("Expected first element to be 'string', got %v", resultArray[0]) } if resultArray[1] != 42 { t.Errorf("Expected second element to be 42, got %v", resultArray[1]) } if resultArray[2] != true { t.Errorf("Expected third element to be true, got %v", resultArray[2]) } // For AMF3, nil values are converted to empty maps for compatibility if resultArray[3] != nil { emptyMap, ok := resultArray[3].(map[string]interface{}) if !ok || len(emptyMap) != 0 { t.Errorf("Expected fourth element to be empty map, got %v", resultArray[3]) } } nested, ok := resultArray[4].(map[string]interface{}) if !ok { t.Error("Expected fifth element to be map") } else if nested["nested"] != "value" { t.Errorf("Expected nested value, got %v", nested["nested"]) } } // Benchmark tests func BenchmarkAMFEncoding(b *testing.B) { encoder := NewAMFEncoder(nil) data := BaseResponse{ Response: ResponseBody{ StatusCode: 200, StatusText: "OK", Data: map[string]interface{}{ "users": []interface{}{ map[string]interface{}{"name": "user1", "online": true}, map[string]interface{}{"name": "user2", "online": false}, map[string]interface{}{"name": "user3", "online": true}, }, "timestamp": time.Now().Unix(), "server": "open-oscar-server", }, }, } b.Run("AMF3", func(b *testing.B) { for i := 0; i < b.N; i++ { _, _ = encoder.EncodeAMF(data, AMF3) } }) } func TestZeroValueDetection(t *testing.T) { encoder := NewAMFEncoder(nil) type TestStruct struct { EmptyString string `json:"emptyString,omitempty"` ZeroInt int `json:"zeroInt,omitempty"` FalseValue bool `json:"falseValue,omitempty"` ZeroTime time.Time `json:"zeroTime,omitempty"` ValidString string `json:"validString,omitempty"` ValidInt int `json:"validInt,omitempty"` TrueValue bool `json:"trueValue,omitempty"` } testStruct := TestStruct{ EmptyString: "", ZeroInt: 0, FalseValue: false, ZeroTime: time.Time{}, ValidString: "test", ValidInt: 42, TrueValue: true, } result := encoder.toAMF3Compatible(testStruct) resultMap, ok := result.(map[string]interface{}) if !ok { t.Fatal("Expected map[string]interface{}") } // Should be omitted (zero values) omittedFields := []string{"emptyString", "zeroInt", "falseValue", "zeroTime"} for _, field := range omittedFields { if _, exists := resultMap[field]; exists { t.Errorf("Field %s should be omitted (zero value)", field) } } // Should be present (non-zero values) presentFields := map[string]interface{}{ "validString": "test", "validInt": 42, "trueValue": true, } for field, expected := range presentFields { if actual, exists := resultMap[field]; !exists { t.Errorf("Field %s should be present", field) } else if actual != expected { t.Errorf("Field %s: expected %v, got %v", field, expected, actual) } } } // The client dereferences response.data on a failure too, so the AMF error // envelope carries one just as the JSON, JSONP and XML ones do. func TestAMFErrorEnvelopeCarriesData(t *testing.T) { encoder := NewAMFEncoder(nil) out, ok := encoder.toAMF3Compatible(newErrorResponse(404, "Not Found")).(map[string]interface{}) if !ok { t.Fatalf("expected an envelope map, got %T", out) } resp, ok := out["response"].(map[string]interface{}) if !ok { t.Fatalf("expected a response map, got %T", out["response"]) } if resp["statusCode"] != 404 { t.Errorf("statusCode: expected 404, got %v", resp["statusCode"]) } data, ok := resp["data"].(map[string]interface{}) if !ok { t.Fatalf("expected an empty data map, got %T", resp["data"]) } if len(data) != 0 { t.Errorf("data: expected empty, got %v", data) } } // The buddylist and preference events carry a pointer payload. goAMF3 emits // nothing for a value it cannot encode, so a pointer that reaches it writes the // key and truncates the stream there, taking every later field with it. func TestAMFEncoderPointerEventData(t *testing.T) { encoder := NewAMFEncoder(nil) event := ConvertEventForAMF3(types.Event{ Type: types.EventTypeBuddyList, SeqNum: 1, Timestamp: 1787277769, Data: &BuddyListData{ Groups: []WebAPIBuddyGroup{{ Name: "Friends", Buddies: []WebAPIBuddyInfo{{AimID: "mk6i"}}, }}, }, }) encoded, err := encoder.EncodeAMF(map[string]interface{}{ "events": []interface{}{event}, "lastSeqNum": 1, }, AMF3) if err != nil { t.Fatalf("EncodeAMF() error = %v", err) } decoded, ok := goAMF3.DecodeAMF3(encoded).(map[string]interface{}) if !ok { t.Fatalf("DecodeAMF3() = %#v, want map", goAMF3.DecodeAMF3(encoded)) } // Present only if the stream survived past the event: it is written after it. if got := fmt.Sprintf("%v", decoded["lastSeqNum"]); got != "1" { t.Errorf("lastSeqNum = %v, want 1", got) } events, ok := decoded["events"].([]interface{}) if !ok || len(events) != 1 { t.Fatalf("events = %#v, want 1 element", decoded["events"]) } eventMap, ok := events[0].(map[string]interface{}) if !ok { t.Fatalf("events[0] = %#v, want map", events[0]) } eventData, ok := eventMap["eventData"].(map[string]interface{}) if !ok { t.Fatalf("eventData = %#v, want map", eventMap["eventData"]) } groups, ok := eventData["groups"].([]interface{}) if !ok || len(groups) != 1 { t.Fatalf("groups = %#v, want 1 element", eventData["groups"]) } group, ok := groups[0].(map[string]interface{}) if !ok { t.Fatalf("groups[0] = %#v, want map", groups[0]) } if got := group["name"]; got != "Friends" { t.Errorf("groups[0].name = %#v, want Friends", got) } } func TestAMFEncoderNilPointerEventData(t *testing.T) { encoder := NewAMFEncoder(nil) encoded, err := encoder.EncodeAMF(map[string]interface{}{ "eventData": (*BuddyListData)(nil), "lastSeqNum": 2, }, AMF3) if err != nil { t.Fatalf("EncodeAMF() error = %v", err) } decoded, ok := goAMF3.DecodeAMF3(encoded).(map[string]interface{}) if !ok { t.Fatalf("DecodeAMF3() = %#v, want map", goAMF3.DecodeAMF3(encoded)) } if got := fmt.Sprintf("%v", decoded["lastSeqNum"]); got != "2" { t.Errorf("lastSeqNum = %v, want 2", got) } }