package backendrpc import ( "bufio" "context" "encoding/json" "errors" "net" "testing" "time" ) type testRequest struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params,omitempty"` } type testEnvelope struct { OK bool `json:"ok"` Status string `json:"status"` Data any `json:"data,omitempty"` Error any `json:"error,omitempty"` RequestID string `json:"request_id"` } type testResponse struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id"` Result any `json:"result,omitempty"` Error *JSONRPCError `json:"error,omitempty"` } func newTestClient(t *testing.T, handler func(t *testing.T, req testRequest) testResponse) *Client { t.Helper() client := New("bat.sock") client.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) { clientConn, serverConn := net.Pipe() go func(conn net.Conn) { defer conn.Close() line, err := bufio.NewReader(conn).ReadBytes('\n') if err != nil { return } var req testRequest if err := json.Unmarshal(line, &req); err != nil { return } resp := handler(t, req) if len(resp.ID) == 0 { resp.ID = req.ID } if resp.JSONRPC == "" { resp.JSONRPC = jsonRPCVersion } _ = json.NewEncoder(conn).Encode(resp) }(serverConn) return clientConn, nil } return client } func TestResourceRepairQueuesTask(t *testing.T) { client := newTestClient(t, func(t *testing.T, req testRequest) testResponse { if req.Method != "resource.repair" { t.Fatalf("method = %s", req.Method) } return testResponse{ Result: testEnvelope{ OK: true, Status: "accepted", RequestID: "req-test-1", Data: map[string]any{ "task_id": "task-1", "kind": "resource.repair", }, }, } }) task, err := client.ResourceRepair(context.Background()) if err != nil { t.Fatalf("ResourceRepair error: %v", err) } if task.TaskID != "task-1" || task.Kind != "resource.repair" { t.Fatalf("unexpected task: %#v", task) } } func TestResourceListSendsPagination(t *testing.T) { client := newTestClient(t, func(t *testing.T, req testRequest) testResponse { if req.Method != "resource.list" { t.Fatalf("method = %s", req.Method) } var params pageParam if err := json.Unmarshal(req.Params, ¶ms); err != nil { t.Fatalf("decode params: %v", err) } if params.Offset != 2 || params.Limit != 1 { t.Fatalf("params = %#v", params) } bytes := uint64(42) return testResponse{ Result: testEnvelope{ OK: true, Status: "ok", RequestID: "req-test-2", Data: map[string]any{ "available": true, "resource_root": "/tmp/resources/current", "manifest_version": 1, "total_entries": 3, "offset": 2, "limit": 1, "entries": []map[string]any{ { "url": "https://example.invalid/a.zip", "destination": "a.zip", "bytes": bytes, "blake3": "abc", }, }, }, }, } }) page, err := client.ResourceList(context.Background(), 2, 1) if err != nil { t.Fatalf("ResourceList error: %v", err) } if !page.Available || page.TotalEntries != 3 || len(page.Entries) != 1 { t.Fatalf("unexpected page: %#v", page) } if page.Entries[0].Bytes == nil || *page.Entries[0].Bytes != 42 { t.Fatalf("unexpected entry bytes: %#v", page.Entries[0].Bytes) } } func TestApplicationErrorReturnsAPIError(t *testing.T) { client := newTestClient(t, func(t *testing.T, req testRequest) testResponse { if req.Method != "task.status" { t.Fatalf("method = %s", req.Method) } return testResponse{ Result: testEnvelope{ OK: false, Status: "error", Error: APIError{ Code: "BAT-ERR-700004", Kind: "task_not_found", Domain: "rpc", Location: "task.status", Message: "任务不存在:missing", Retryable: false, }, RequestID: "req-test-3", }, } }) _, err := client.TaskStatus(context.Background(), "missing") var apiErr *APIError if !errors.As(err, &apiErr) { t.Fatalf("expected APIError, got %T %v", err, err) } if apiErr.Code != "BAT-ERR-700004" { t.Fatalf("code = %s", apiErr.Code) } } func TestTransportErrorReturnsJSONRPCError(t *testing.T) { client := newTestClient(t, func(t *testing.T, req testRequest) testResponse { return testResponse{ Error: &JSONRPCError{ Code: -32700, Message: "parse error", }, } }) _, err := client.DaemonStatus(context.Background()) var rpcErr *JSONRPCError if !errors.As(err, &rpcErr) { t.Fatalf("expected JSONRPCError, got %T %v", err, err) } if rpcErr.Code != -32700 { t.Fatalf("code = %d", rpcErr.Code) } } func TestContextDeadlineIsApplied(t *testing.T) { client := New("bat.sock") client.Timeout = time.Millisecond client.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) { <-ctx.Done() return nil, ctx.Err() } ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) defer cancel() _, err := client.DaemonStatus(ctx) if err == nil { t.Fatal("expected dial error") } }