Files
multica/server/internal/middleware/client_test.go
LinYushen b624cd98ad feat: identify clients via X-Client-Platform/Version/OS (#1477)
* feat: identify clients via X-Client-Platform/Version/OS

Adds client identification headers (and matching WS query params) across
all first-party clients so the server can split logs/metrics/gating by
caller without parsing User-Agent.

- HTTP: X-Client-Platform, X-Client-Version, X-Client-OS
- WS: client_platform, client_version, client_os query params
- Platform ∈ {web, desktop, cli, daemon}; OS ∈ {macos, windows, linux}

Wired through the shared TS ApiClient/WSClient via a new identity option
on CoreProvider. Web reads its version from package.json/env; Desktop
captures version + OS synchronously in preload via sendSync IPC. Go CLI
and daemon clients populate the same headers using runtime.GOOS
(normalized darwin → macos).

Server-side adds a ClientMetadata middleware that stashes the headers in
request context; the request logger and logger.RequestAttrs surface them
on every access log and handler-level log. Realtime hub logs the same
fields on websocket connect.

CORS allowlist extended for the new headers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: address client-identity PR nits

- Memoize the CoreProvider identity object on Web and Desktop, and key
  WSProvider's effect on identity primitives instead of the object
  reference, so unrelated parent re-renders no longer tear down and
  reconnect the WebSocket.
- Add direct header-injection tests for the CLI and daemon Go HTTP
  clients (X-Client-Platform/Version/OS) and a normalizeGOOS unit test
  on both packages.
- Add a TS test for WSClient that asserts client_platform/client_version/
  client_os land on the upgrade URL and never leak the auth token.
- Add a hub test that dials the WS endpoint with client_* query params
  and asserts the "websocket connected" log entry surfaces them as
  structured attributes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 13:36:13 +08:00

55 lines
1.7 KiB
Go

package middleware
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestClientMetadataExtractsHeaders(t *testing.T) {
var gotPlatform, gotVersion, gotOS string
handler := ClientMetadata(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
gotPlatform, gotVersion, gotOS = ClientMetadataFromContext(r.Context())
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(HeaderClientPlatform, "desktop")
req.Header.Set(HeaderClientVersion, "1.2.3")
req.Header.Set(HeaderClientOS, "macos")
handler.ServeHTTP(httptest.NewRecorder(), req)
if gotPlatform != "desktop" {
t.Errorf("platform: got %q, want desktop", gotPlatform)
}
if gotVersion != "1.2.3" {
t.Errorf("version: got %q, want 1.2.3", gotVersion)
}
if gotOS != "macos" {
t.Errorf("os: got %q, want macos", gotOS)
}
}
func TestClientMetadataMissingHeadersReturnEmpty(t *testing.T) {
var gotPlatform, gotVersion, gotOS string
handler := ClientMetadata(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
gotPlatform, gotVersion, gotOS = ClientMetadataFromContext(r.Context())
}))
handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
if gotPlatform != "" || gotVersion != "" || gotOS != "" {
t.Errorf("expected empty metadata, got (%q,%q,%q)", gotPlatform, gotVersion, gotOS)
}
}
func TestSetClientMetadataAttachesValues(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
ctx := SetClientMetadata(req.Context(), "cli", "0.5.1", "linux")
platform, version, os := ClientMetadataFromContext(ctx)
if platform != "cli" || version != "0.5.1" || os != "linux" {
t.Errorf("got (%q,%q,%q), want (cli,0.5.1,linux)", platform, version, os)
}
}