package managed import ( "bytes" "context" "encoding/json" "io" "net/http" "strings" "testing" ) type roundTripFunc func(req *http.Request) (*http.Response, error) func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return fn(req) } func TestRemoteClientRoundTrip(t *testing.T) { t.Parallel() storeConfig := map[string]any{ "config_path": "/srv/store/config/local.yaml", "stream": map[string]any{ "rtsp_url": "rtsp://store-old/stream", }, } client := NewRemoteClient(&http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { response := func(status int, body any) (*http.Response, error) { data, err := json.Marshal(body) if err != nil { return nil, err } return &http.Response{ StatusCode: status, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(data)), }, nil } switch { case r.Method == http.MethodGet && r.URL.Path == "/store/api/manage/config": return response(http.StatusOK, storeConfig) case r.Method == http.MethodPut && r.URL.Path == "/store/api/manage/config": var payload map[string]string if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { t.Fatalf("decode update payload: %v", err) } storeConfig["stream"].(map[string]any)["rtsp_url"] = payload["rtsp_url"] return response(http.StatusOK, storeConfig) case r.Method == http.MethodGet && r.URL.Path == "/store/api/manage/summary": return response(http.StatusOK, ResultSummary{ ResultType: "store_dwell_alert", Headline: "Latest report shows 1 active customers, longest dwell 900s", LastResultTime: "2026-04-16T10:00:00+08:00", Metrics: map[string]any{ "active_customer_count": 1, "longest_dwell_seconds": 900, }, }) case r.Method == http.MethodGet && r.URL.Path == "/store/api/manage/files": return response(http.StatusOK, map[string]any{ "files": []ResultFile{{ Path: "logs/events.jsonl", Name: "events.jsonl", }}, }) case r.Method == http.MethodGet && r.URL.Path == "/store/api/manage/files/preview": return response(http.StatusOK, FilePreview{ Path: "logs/events.jsonl", Lines: []string{"line1", "line2"}, Count: 2, }) case r.Method == http.MethodGet && r.URL.Path == "/store/api/manage/files/download": return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{ "Content-Disposition": []string{`attachment; filename="events.jsonl"`}, }, Body: io.NopCloser(strings.NewReader("downloaded")), }, nil default: t.Fatalf("unexpected child request: %s %s", r.Method, r.URL.String()) return nil, nil } })}) service := Service{ ID: "store_dwell_alert", APIBaseURL: "http://managed.invalid/store", } configPayload, err := client.GetConfig(context.Background(), service) if err != nil { t.Fatalf("GetConfig() error = %v", err) } if got := configPayload["config_path"]; got != "/srv/store/config/local.yaml" { t.Fatalf("config_path = %#v", got) } if _, err := client.UpdateRTSP(context.Background(), service, "rtsp://store-new/stream"); err != nil { t.Fatalf("UpdateRTSP() error = %v", err) } if got := storeConfig["stream"].(map[string]any)["rtsp_url"]; got != "rtsp://store-new/stream" { t.Fatalf("updated rtsp = %#v", got) } summary, err := client.GetSummary(context.Background(), service) if err != nil { t.Fatalf("GetSummary() error = %v", err) } if summary.ResultType != "store_dwell_alert" { t.Fatalf("summary.ResultType = %q", summary.ResultType) } files, err := client.GetFiles(context.Background(), service) if err != nil { t.Fatalf("GetFiles() error = %v", err) } if len(files) != 1 || files[0].Path != "logs/events.jsonl" { t.Fatalf("files = %#v", files) } preview, err := client.PreviewFile(context.Background(), service, "logs/events.jsonl", 2) if err != nil { t.Fatalf("PreviewFile() error = %v", err) } if preview.Count != 2 { t.Fatalf("preview.Count = %d", preview.Count) } resp, err := client.Download(context.Background(), service, "logs/events.jsonl") if err != nil { t.Fatalf("Download() error = %v", err) } defer resp.Body.Close() if !strings.Contains(resp.Header.Get("Content-Disposition"), "events.jsonl") { t.Fatalf("Content-Disposition = %q", resp.Header.Get("Content-Disposition")) } }