Files
managed-portal/internal/managed/remote_client_test.go
2026-05-07 17:58:26 +08:00

178 lines
5.2 KiB
Go

package managed
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"testing"
"time"
)
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"))
}
}
func TestRemoteClientRetriesTransientRequestErrors(t *testing.T) {
t.Parallel()
attempts := 0
client := NewRemoteClient(&http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
attempts++
if attempts < 3 {
return nil, errors.New("connect: connection refused")
}
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"config_path":"/srv/store/config/local.yaml"}`)),
}, nil
})})
client.retryDelay = time.Millisecond
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 attempts != 3 {
t.Fatalf("attempts = %d", attempts)
}
if got := configPayload["config_path"]; got != "/srv/store/config/local.yaml" {
t.Fatalf("config_path = %#v", got)
}
}