Files
yatulandClaude Opus 5 ea4890c257 fix(pairing): address review — docs, error mapping, approve feedback, unreadable expiry
- docs: device.pair.update and the `permanent` option on approve in
  docs/04-gateway-protocol.md, docs/19-websocket-rpc.md and
  websocket-protocol.md; the paired-device TTL row in docs/09-security.md
  now mentions the admin opt-out.
- store.ErrPairedDeviceNotFound: SetPairingPermanent wraps it in both stores.
  device.pair.update maps it to NOT_FOUND and any other store error to
  INTERNAL, so a DB failure no longer reads as "not found".
- web UI: approve and make-permanent/set-expiry now toast the server error
  and reload the list in `finally`. A partially applied approve (paired, but
  the permanent write failed) shows up in the table instead of leaving the
  dialog dead-ended.
- SQLite ListPaired: a stored expiry that fails to parse stays 0 (expires,
  date unknown) rather than being mistaken for permanent; the UI renders it
  as "--" instead of a 1970 date.

Tests: gateway handler error mapping (NOT_FOUND / INTERNAL / OK), sentinel
checks in the PG and SQLite store tests, SQLite unreadable-expiry case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 19:26:43 +04:00

67 lines
1.9 KiB
Go

package methods
import (
"context"
"encoding/json"
"errors"
"fmt"
"testing"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/gateway"
"github.com/nextlevelbuilder/goclaw/internal/permissions"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
// stubPermanentPairingStore answers SetPairingPermanent with a fixed error.
type stubPermanentPairingStore struct {
store.PairingStore
err error
}
func (s *stubPermanentPairingStore) SetPairingPermanent(context.Context, string, string, bool) error {
return s.err
}
func callPairingUpdate(t *testing.T, storeErr error) protocol.ResponseFrame {
t.Helper()
m := NewPairingMethods(&stubPermanentPairingStore{err: storeErr}, bus.New(), nil)
client, out := gateway.NewCapturingTestClient(permissions.RoleAdmin, store.MasterTenantID, "admin-1", 1)
raw, _ := json.Marshal(map[string]any{"senderId": "u1", "channel": "telegram", "permanent": true})
m.handleUpdate(t.Context(), client, &protocol.RequestFrame{ID: "r1", Params: raw})
var frame protocol.ResponseFrame
if err := json.Unmarshal(<-out, &frame); err != nil {
t.Fatalf("decode response: %v", err)
}
return frame
}
func TestPairingUpdate_ErrorMapping(t *testing.T) {
cases := []struct {
name string
storeErr error
wantCode string
}{
{"not found", fmt.Errorf("%w: telegram/u1", store.ErrPairedDeviceNotFound), protocol.ErrNotFound},
{"db failure", errors.New("connection reset"), protocol.ErrInternal},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
frame := callPairingUpdate(t, tc.storeErr)
if frame.OK || frame.Error == nil || frame.Error.Code != tc.wantCode {
t.Fatalf("want error %s, got ok=%v error=%+v", tc.wantCode, frame.OK, frame.Error)
}
})
}
}
func TestPairingUpdate_OK(t *testing.T) {
frame := callPairingUpdate(t, nil)
if !frame.OK {
t.Fatalf("want ok, got error %+v", frame.Error)
}
}