package garmin import ( "bufio" "encoding/json" "io" "strings" "testing" ) // wireResponsePayload is what a fake wrapper handler returns for one // request; the harness fills in the response ID. type wireResponsePayload struct { result json.RawMessage err string } func fakeResult(v any) wireResponsePayload { b, err := json.Marshal(v) if err != nil { panic(err) } return wireResponsePayload{result: b} } func fakeError(msg string) wireResponsePayload { return wireResponsePayload{err: msg} } // newFakeWrapperClient wires a subprocessClient to an in-process goroutine // that plays the Python wrapper's role, so protocol-level Go logic can be // tested without python3/garminconnect installed. func newFakeWrapperClient(t *testing.T, handle func(cmd string, params json.RawMessage) wireResponsePayload) *subprocessClient { t.Helper() reqR, reqW := io.Pipe() respR, respW := io.Pipe() t.Cleanup(func() { reqW.Close() respW.Close() }) go func() { scanner := bufio.NewScanner(reqR) scanner.Buffer(make([]byte, 0, 64*1024), maxWrapperLineBytes) enc := json.NewEncoder(respW) for scanner.Scan() { var req struct { ID int `json:"id"` Cmd string `json:"cmd"` Params json.RawMessage `json:"params"` } if err := json.Unmarshal(scanner.Bytes(), &req); err != nil { continue } payload := handle(req.Cmd, req.Params) resp := wireResponse{ID: req.ID, Result: payload.result, Error: payload.err} if err := enc.Encode(resp); err != nil { return } } }() scanner := bufio.NewScanner(respR) scanner.Buffer(make([]byte, 0, 64*1024), maxWrapperLineBytes) return &subprocessClient{ started: true, enc: json.NewEncoder(reqW), scanner: scanner, } } func TestSubprocessClient_RoundTrip_DetectsIDMismatch(t *testing.T) { reqR, reqW := io.Pipe() respR, respW := io.Pipe() defer reqW.Close() defer respW.Close() go func() { scanner := bufio.NewScanner(reqR) scanner.Buffer(make([]byte, 0, 64*1024), maxWrapperLineBytes) scanner.Scan() // read and discard the one request json.NewEncoder(respW).Encode(wireResponse{ID: 999, Result: json.RawMessage(`{}`)}) }() scanner := bufio.NewScanner(respR) scanner.Buffer(make([]byte, 0, 64*1024), maxWrapperLineBytes) c := &subprocessClient{started: true, enc: json.NewEncoder(reqW), scanner: scanner} _, err := c.roundTrip("authenticate", nil) if err == nil || !strings.Contains(err.Error(), "id mismatch") { t.Fatalf("roundTrip error = %v, want an id mismatch error", err) } } func TestSubprocessClient_RoundTrip_SubprocessClosedIsError(t *testing.T) { reqR, reqW := io.Pipe() respR, respW := io.Pipe() defer reqW.Close() go func() { scanner := bufio.NewScanner(reqR) scanner.Buffer(make([]byte, 0, 64*1024), maxWrapperLineBytes) scanner.Scan() respW.Close() // subprocess "exited": stdout closes with no response }() scanner := bufio.NewScanner(respR) scanner.Buffer(make([]byte, 0, 64*1024), maxWrapperLineBytes) c := &subprocessClient{started: true, enc: json.NewEncoder(reqW), scanner: scanner} _, err := c.roundTrip("authenticate", nil) if err == nil || !strings.Contains(err.Error(), "closed") { t.Fatalf("roundTrip error = %v, want a subprocess-closed error", err) } } func TestSubprocessClient_RoundTrip_WrapperErrorPropagates(t *testing.T) { c := newFakeWrapperClient(t, func(cmd string, params json.RawMessage) wireResponsePayload { return fakeError("boom") }) _, err := c.roundTrip("authenticate", nil) if err == nil || !strings.Contains(err.Error(), "boom") { t.Fatalf("roundTrip error = %v, want it to mention %q", err, "boom") } } func TestSubprocessClient_UpdateCredentials_ResetsStartedState(t *testing.T) { c := &subprocessClient{cfg: Config{GarminEmail: "old@example.com", GarminPassword: "old"}} c.started = true // simulate an already-spawned subprocess, no real cmd/pipes c.UpdateCredentials("new@example.com", "new") if c.cfg.GarminEmail != "new@example.com" || c.cfg.GarminPassword != "new" { t.Errorf("cfg after update = %+v, want new@example.com/new", c.cfg) } if c.started { t.Error("started should be reset to false so the next call respawns the subprocess") } } func TestSubprocessClient_Close_NoOpWhenNotStarted(t *testing.T) { c := &subprocessClient{} if err := c.Close(); err != nil { t.Errorf("Close on an unstarted client = %v, want nil", err) } }