feat(garmin): implement Authenticate/CompleteMFA on subprocessClient
Structured {status, message} responses replace the old string
pattern-matching (parseAuthResult) -- both sides of the protocol are now
owned by this repo, so there's no need to guess at phrasing anymore.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -225,6 +225,42 @@ func (c *subprocessClient) roundTrip(cmdName string, params any) (json.RawMessag
|
|||||||
return resp.Result, nil
|
return resp.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *subprocessClient) Authenticate(ctx context.Context) (AuthResult, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if err := c.ensureStarted(); err != nil {
|
||||||
|
return AuthResult{}, err
|
||||||
|
}
|
||||||
|
raw, err := c.roundTrip("authenticate", nil)
|
||||||
|
if err != nil {
|
||||||
|
return AuthResult{}, err
|
||||||
|
}
|
||||||
|
var wire authResultWire
|
||||||
|
if err := json.Unmarshal(raw, &wire); err != nil {
|
||||||
|
return AuthResult{}, fmt.Errorf("parse authenticate result: %w", err)
|
||||||
|
}
|
||||||
|
return AuthResult{Status: mapAuthStatus(wire.Status), Message: wire.Message}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *subprocessClient) CompleteMFA(ctx context.Context, code string) (AuthResult, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if err := c.ensureStarted(); err != nil {
|
||||||
|
return AuthResult{}, err
|
||||||
|
}
|
||||||
|
raw, err := c.roundTrip("complete_mfa", map[string]any{"code": code})
|
||||||
|
if err != nil {
|
||||||
|
return AuthResult{}, err
|
||||||
|
}
|
||||||
|
var wire authResultWire
|
||||||
|
if err := json.Unmarshal(raw, &wire); err != nil {
|
||||||
|
return AuthResult{}, fmt.Errorf("parse complete_mfa result: %w", err)
|
||||||
|
}
|
||||||
|
return AuthResult{Status: mapAuthStatus(wire.Status), Message: wire.Message}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateCredentials implements Client.
|
// UpdateCredentials implements Client.
|
||||||
func (c *subprocessClient) UpdateCredentials(email, password string) {
|
func (c *subprocessClient) UpdateCredentials(email, password string) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package garmin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -191,3 +192,87 @@ func TestSubprocessClient_Close_WaitsForStderrCopyGoroutine(t *testing.T) {
|
|||||||
t.Error("stderrDone should be reset to nil after close()")
|
t.Error("stderrDone should be reset to nil after close()")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSubprocessClient_Authenticate_Success(t *testing.T) {
|
||||||
|
c := newFakeWrapperClient(t, func(cmd string, params json.RawMessage) wireResponsePayload {
|
||||||
|
if cmd != "authenticate" {
|
||||||
|
t.Fatalf("unexpected cmd %q", cmd)
|
||||||
|
}
|
||||||
|
return fakeResult(authResultWire{Status: "success", Message: "Authenticated successfully."})
|
||||||
|
})
|
||||||
|
|
||||||
|
res, err := c.Authenticate(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Authenticate: %v", err)
|
||||||
|
}
|
||||||
|
if res.Status != AuthSuccess || res.Message != "Authenticated successfully." {
|
||||||
|
t.Errorf("Authenticate result = %+v", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessClient_Authenticate_MFARequired(t *testing.T) {
|
||||||
|
c := newFakeWrapperClient(t, func(cmd string, params json.RawMessage) wireResponsePayload {
|
||||||
|
return fakeResult(authResultWire{Status: "mfa_required", Message: "MFA required. ..."})
|
||||||
|
})
|
||||||
|
|
||||||
|
res, err := c.Authenticate(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Authenticate: %v", err)
|
||||||
|
}
|
||||||
|
if res.Status != AuthMFARequired {
|
||||||
|
t.Errorf("Authenticate status = %v, want AuthMFARequired", res.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessClient_Authenticate_WrapperErrorPropagates(t *testing.T) {
|
||||||
|
c := newFakeWrapperClient(t, func(cmd string, params json.RawMessage) wireResponsePayload {
|
||||||
|
return fakeError("subprocess exploded")
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := c.Authenticate(context.Background())
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "subprocess exploded") {
|
||||||
|
t.Fatalf("Authenticate error = %v, want it to mention the wrapper error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessClient_CompleteMFA_SendsCodeAndReturnsStatus(t *testing.T) {
|
||||||
|
var gotCode string
|
||||||
|
c := newFakeWrapperClient(t, func(cmd string, params json.RawMessage) wireResponsePayload {
|
||||||
|
if cmd != "complete_mfa" {
|
||||||
|
t.Fatalf("unexpected cmd %q", cmd)
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(params, &p); err != nil {
|
||||||
|
t.Fatalf("unmarshal params: %v", err)
|
||||||
|
}
|
||||||
|
gotCode = p.Code
|
||||||
|
return fakeResult(authResultWire{Status: "success", Message: "MFA accepted. Authenticated successfully."})
|
||||||
|
})
|
||||||
|
|
||||||
|
res, err := c.CompleteMFA(context.Background(), "123456")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CompleteMFA: %v", err)
|
||||||
|
}
|
||||||
|
if gotCode != "123456" {
|
||||||
|
t.Errorf("code sent to wrapper = %q, want 123456", gotCode)
|
||||||
|
}
|
||||||
|
if res.Status != AuthSuccess {
|
||||||
|
t.Errorf("CompleteMFA status = %v, want AuthSuccess", res.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessClient_CompleteMFA_Failed(t *testing.T) {
|
||||||
|
c := newFakeWrapperClient(t, func(cmd string, params json.RawMessage) wireResponsePayload {
|
||||||
|
return fakeResult(authResultWire{Status: "failed", Message: "Authentication failed after MFA: bad code"})
|
||||||
|
})
|
||||||
|
|
||||||
|
res, err := c.CompleteMFA(context.Background(), "000000")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CompleteMFA: %v", err)
|
||||||
|
}
|
||||||
|
if res.Status != AuthFailed {
|
||||||
|
t.Errorf("CompleteMFA status = %v, want AuthFailed", res.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user