Files
BlueArchiveToolkit/internal/backendrpc/client_test.go
T
nyaKazuha 0784d5b532
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s
feat(bat): 完善工作流调度与 dashboard RPC
补全资源拉取、解析、翻译、重打包和本地化发布命令,支持单次、限定次数与周期调度。移除 TUI 计划并通过 schedule.* RPC 暴露给 bat-api dashboard。

Closes #43
2026-08-03 22:18:52 +08:00

368 lines
9.8 KiB
Go

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 TestDaemonRestartSendsControlMethod(t *testing.T) {
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
if req.Method != "daemon.restart" {
t.Fatalf("method = %s", req.Method)
}
return testResponse{
Result: testEnvelope{
OK: true,
Status: "accepted",
RequestID: "req-test-restart",
Data: map[string]any{
"command": "restart",
"status": "accepted",
"message": "restart accepted",
"state_dir": "/tmp/bat-pid",
"socket_path": "/tmp/bat-pid/bat.sock",
"controller_pid": 4242,
},
},
}
})
ack, err := client.DaemonRestart(context.Background())
if err != nil {
t.Fatalf("DaemonRestart error: %v", err)
}
if ack.Command != "restart" || ack.ControllerPID == nil || *ack.ControllerPID != 4242 {
t.Fatalf("unexpected ack: %#v", ack)
}
}
func TestParseTextUnitsSendsQuery(t *testing.T) {
pathID := int64(7)
classID := 114
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
if req.Method != "parse.text_units" {
t.Fatalf("method = %s", req.Method)
}
var params TextUnitQueryParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode params: %v", err)
}
if params.Offset != 3 || params.Limit != 5 || params.Destination != "*Table*" || params.PathID == nil || *params.PathID != pathID || params.ClassID == nil || *params.ClassID != classID {
t.Fatalf("params = %#v", params)
}
return testResponse{
Result: testEnvelope{
OK: true,
Status: "ok",
RequestID: "req-test-parse",
Data: map[string]any{
"available": true,
"entries": []any{},
},
},
}
})
raw, err := client.ParseTextUnits(context.Background(), TextUnitQueryParams{
Offset: 3,
Limit: 5,
Destination: "*Table*",
PathID: &pathID,
ClassID: &classID,
})
if err != nil {
t.Fatalf("ParseTextUnits error: %v", err)
}
if !json.Valid(raw) {
t.Fatalf("invalid raw JSON: %s", string(raw))
}
}
func TestUnityFSPatchFieldSendsTaggedReplacement(t *testing.T) {
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
if req.Method != "unityfs.patch_field" {
t.Fatalf("method = %s", req.Method)
}
var params UnityFSFieldPatchParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode params: %v", err)
}
if params.BundlePath != "/tmp/source.bundle" || params.FieldPath != "m_Name" || string(params.Replacement) != `{"kind":"string","value":"new text"}` {
t.Fatalf("params = %#v replacement=%s", params, string(params.Replacement))
}
return testResponse{
Result: testEnvelope{
OK: true,
Status: "ok",
RequestID: "req-test-unityfs",
Data: map[string]any{
"command": "unityfs.patch_field",
"status": "completed",
},
},
}
})
raw, err := client.UnityFSPatchField(context.Background(), UnityFSFieldPatchParams{
BundlePath: "/tmp/source.bundle",
SerializedFilePath: "CAB-test",
PathID: 1,
FieldPath: "m_Name",
Replacement: json.RawMessage(`{"kind":"string","value":"new text"}`),
TargetPath: "/tmp/target.bundle",
})
if err != nil {
t.Fatalf("UnityFSPatchField error: %v", err)
}
if !json.Valid(raw) {
t.Fatalf("invalid raw JSON: %s", string(raw))
}
}
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, &params); 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 TestScheduleUpdateSendsMutationParams(t *testing.T) {
every := uint64(3600)
count := uint64(4)
enabled := true
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
if req.Method != "schedule.update" {
t.Fatalf("method = %s", req.Method)
}
var params ScheduleMutationParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode params: %v", err)
}
if params.ID != "nightly-pull" || params.Group != "res" || params.Action != "pull" ||
params.EverySeconds == nil || *params.EverySeconds != every ||
params.Count == nil || *params.Count != count || params.Enabled == nil || !*params.Enabled {
t.Fatalf("params = %#v", params)
}
return testResponse{
Result: testEnvelope{
OK: true,
Status: "ok",
RequestID: "req-test-schedule",
Data: map[string]any{
"command": "schedule-update",
"status": "updated",
},
},
}
})
raw, err := client.ScheduleUpdate(context.Background(), ScheduleMutationParams{
ID: "nightly-pull",
Group: "res",
Action: "pull",
EverySeconds: &every,
Count: &count,
Enabled: &enabled,
})
if err != nil {
t.Fatalf("ScheduleUpdate error: %v", err)
}
if !json.Valid(raw) {
t.Fatalf("invalid raw JSON: %s", string(raw))
}
}
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")
}
}