diff --git a/pkg/service/handlers/handlers_marge.go b/pkg/service/handlers/handlers_marge.go index e1c9faa..1ff6a1b 100644 --- a/pkg/service/handlers/handlers_marge.go +++ b/pkg/service/handlers/handlers_marge.go @@ -300,6 +300,7 @@ func (s *Server) HandleMargeAddRecent(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml") + w.WriteHeader(http.StatusCreated) _, _ = w.Write(data) } diff --git a/pkg/service/handlers/handlers_marge_test.go b/pkg/service/handlers/handlers_marge_test.go index 2faef2d..18784d8 100644 --- a/pkg/service/handlers/handlers_marge_test.go +++ b/pkg/service/handlers/handlers_marge_test.go @@ -320,8 +320,8 @@ func TestMargeAddRecentRoute(t *testing.T) { defer func() { _ = res.Body.Close() }() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status OK, got %v", res.Status) + if res.StatusCode != http.StatusCreated { + t.Errorf("Expected status Created, got %v", res.Status) } // Verify file was saved @@ -389,9 +389,9 @@ func TestMargeNativeStreamingRoutes(t *testing.T) { } defer res.Body.Close() - if res.StatusCode != http.StatusOK { + if res.StatusCode != http.StatusCreated { body, _ := io.ReadAll(res.Body) - t.Errorf("Expected status OK, got %v: %s", res.Status, string(body)) + t.Errorf("Expected status Created, got %v: %s", res.Status, string(body)) } if ct := res.Header.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" { diff --git a/pkg/service/handlers/mirror_middleware.go b/pkg/service/handlers/mirror_middleware.go index 749093c..3aa0e95 100644 --- a/pkg/service/handlers/mirror_middleware.go +++ b/pkg/service/handlers/mirror_middleware.go @@ -116,26 +116,42 @@ type parityResponseWriter struct { } func (p *parityResponseWriter) Header() http.Header { - return p.ResponseWriter.Header() + return p.recorder.Header() } func (p *parityResponseWriter) Write(b []byte) (int, error) { + if p.recorder.status == 0 { + p.WriteHeader(http.StatusOK) + } + p.recorder.body.Write(b) + return p.ResponseWriter.Write(b) } func (p *parityResponseWriter) WriteHeader(statusCode int) { p.recorder.status = statusCode + // Copy headers to the real response writer before writing the header + for k, vv := range p.recorder.headers { + for _, v := range vv { + p.ResponseWriter.Header().Add(k, v) + } + } + p.ResponseWriter.WriteHeader(statusCode) } func (s *Server) performMirror(r *http.Request) *mirrorResponseRecorder { host := r.Host - if host == "" || host == "localhost" || strings.HasPrefix(host, "127.0.0.1") { + if host == "" || host == "localhost" { host = "streaming.bose.com" } scheme := "https" + if strings.HasPrefix(host, "127.0.0.1") || strings.HasPrefix(host, "localhost") { + scheme = "http" + } + targetURL := scheme + "://" + host target, err := url.Parse(targetURL) @@ -231,12 +247,14 @@ func (s *Server) saveParityMismatch(req *http.Request, local, upstream *mirrorRe "path": req.URL.Path, "reasons": reasons, "local": map[string]interface{}{ - "status": local.status, - "body": local.body.String(), + "status": local.status, + "headers": local.headers, + "body": local.body.String(), }, "upstream": map[string]interface{}{ - "status": upstream.status, - "body": upstream.body.String(), + "status": upstream.status, + "headers": upstream.headers, + "body": upstream.body.String(), }, } diff --git a/pkg/service/handlers/mirror_test.go b/pkg/service/handlers/mirror_test.go index 8ce5149..f179df0 100644 --- a/pkg/service/handlers/mirror_test.go +++ b/pkg/service/handlers/mirror_test.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "net/http" "net/http/httptest" "os" @@ -57,7 +58,8 @@ func TestMirroring(t *testing.T) { _ = os.WriteFile(filepath.Join(deviceDir, "Sources.xml"), []byte(""), 0644) t.Run("Mirrored Endpoint", func(t *testing.T) { - req, _ := http.NewRequest("GET", ts.URL+"/streaming/account/"+account+"/device/"+deviceID+"/recent", nil) + path := "/streaming/account/" + account + "/device/" + deviceID + "/recent" + req, _ := http.NewRequest("GET", ts.URL+path, nil) // We set the host to our mock upstream so performMirror finds it req.Host = strings.TrimPrefix(boseUpstream.URL, "http://") @@ -91,6 +93,50 @@ func TestMirroring(t *testing.T) { t.Errorf("Expected to find mirrored interaction in logs (category: mirror). Found: %v", files) } }) + + t.Run("Parity Mismatch Header Capture", func(t *testing.T) { + // The previous test already triggered a mismatch because the bodies and content-types differ + // local: (from file), content-type: text/xml (default) + // upstream: , content-type: application/vnd.bose.streaming-v1.2+xml + + matchesMismatch, _ := filepath.Glob(filepath.Join(tempDir, "parity_mismatches", "*.json")) + if len(matchesMismatch) == 0 { + t.Fatal("Expected to find parity mismatch JSON file") + } + + data, err := os.ReadFile(matchesMismatch[0]) + if err != nil { + t.Fatalf("Failed to read mismatch file: %v", err) + } + + var record struct { + Local struct { + Headers http.Header `json:"headers"` + } `json:"local"` + Upstream struct { + Headers http.Header `json:"headers"` + } `json:"upstream"` + } + + if err := json.Unmarshal(data, &record); err != nil { + t.Fatalf("Failed to unmarshal mismatch record: %v", err) + } + + if len(record.Local.Headers) == 0 { + t.Error("Expected local headers in parity mismatch, got none") + } + if len(record.Upstream.Headers) == 0 { + t.Error("Expected upstream headers in parity mismatch, got none") + } + + // Check specifically for Content-Type + if ct := record.Local.Headers.Get("Content-Type"); ct == "" { + t.Error("Expected Content-Type in local headers") + } + if ct := record.Upstream.Headers.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" { + t.Errorf("Expected Upstream Content-Type application/vnd.bose.streaming-v1.2+xml, got %s", ct) + } + }) } // SetRecordEnabled is a helper for testing