diff --git a/.gitignore b/.gitignore index ee4c829..bae6e66 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ cmake-build-debug/ \.idea/workspace\.xml \.idea/ vendor/ + +coverage.txt diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..99c33b4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "go.testFlags": [ + "-tags=ledger_mock", + "-v", + "-race", + "-coverprofile=coverage.txt", + "-covermode=atomic" + ], + "go.coverOnSave": true, + "go.coverageOptions": "showUncoveredCodeOnly", + "go.coverageDecorator": { + "type": "gutter", + "coveredHighlightColor": "rgba(64,128,128,0.5)", + "uncoveredHighlightColor": "rgba(128,64,64,0.25)", + "coveredGutterStyle": "blockblue", + "uncoveredGutterStyle": "slashyellow" + } +} \ No newline at end of file diff --git a/Makefile b/Makefile index a7fa35c..6f35bd5 100644 --- a/Makefile +++ b/Makefile @@ -49,4 +49,4 @@ lint: golangci-lint run test: - go test -tags ledger_mock -v -race ./... + go test -tags ledger_mock -v -race ./... -coverprofile=coverage.txt -covermode=atomic diff --git a/apduWrapper.go b/apduWrapper.go index 49d6ab2..4e46353 100644 --- a/apduWrapper.go +++ b/apduWrapper.go @@ -30,11 +30,18 @@ const ( var codec = binary.BigEndian +const ( + ErrMsgPacketSize = "packet size must be at least 3" + ErrMsgInvalidChannel = "invalid channel" + ErrMsgInvalidTag = "invalid tag" + ErrMsgWrongSequenceIdx = "wrong sequenceIdx" +) + var ( - ErrPacketSize = errors.New("packet size must be at least 3") - ErrInvalidChannel = errors.New("invalid channel") - ErrInvalidTag = errors.New("invalid tag") - ErrWrongSequenceIdx = errors.New("wrong sequenceIdx") + ErrPacketSize = errors.New(ErrMsgPacketSize) + ErrInvalidChannel = errors.New(ErrMsgInvalidChannel) + ErrInvalidTag = errors.New(ErrMsgInvalidTag) + ErrWrongSequenceIdx = errors.New(ErrMsgWrongSequenceIdx) ) // ErrorMessage returns a human-readable error message for a given APDU error code. diff --git a/ledger.go b/ledger.go index 02563ef..28df9a4 100644 --- a/ledger.go +++ b/ledger.go @@ -16,12 +16,14 @@ package ledger_go +// LedgerAdmin defines the interface for managing Ledger devices. type LedgerAdmin interface { CountDevices() int ListDevices() ([]string, error) Connect(deviceIndex int) (LedgerDevice, error) } +// LedgerDevice defines the interface for interacting with a Ledger device. type LedgerDevice interface { Exchange(command []byte) ([]byte, error) Close() error diff --git a/ledger_hid.go b/ledger_hid.go index f8f83d7..6f8a1a6 100644 --- a/ledger_hid.go +++ b/ledger_hid.go @@ -53,33 +53,36 @@ var supportedLedgerProductID = map[uint16]int{ 0x5: 0, // Ledger Nano S Plus } -func NewLedgerAdmin() *LedgerAdminHID { +func NewLedgerAdmin() LedgerAdmin { return &LedgerAdminHID{} } func (admin *LedgerAdminHID) ListDevices() ([]string, error) { devices := hid.Enumerate(0, 0) - if len(devices) == 0 { - fmt.Printf("No devices. Ledger LOCKED OR Other Program/Web Browser may have control of device.") + log.Println("No devices. Ledger LOCKED OR Other Program/Web Browser may have control of device.") } for _, d := range devices { - fmt.Printf("============ %s\n", d.Path) - fmt.Printf("VendorID : %x\n", d.VendorID) - fmt.Printf("ProductID : %x\n", d.ProductID) - fmt.Printf("Release : %x\n", d.Release) - fmt.Printf("Serial : %x\n", d.Serial) - fmt.Printf("Manufacturer : %s\n", d.Manufacturer) - fmt.Printf("Product : %s\n", d.Product) - fmt.Printf("UsagePage : %x\n", d.UsagePage) - fmt.Printf("Usage : %x\n", d.Usage) - fmt.Printf("\n") + logDeviceInfo(d) } return []string{}, nil } +func logDeviceInfo(d hid.DeviceInfo) { + log.Printf("============ %s\n", d.Path) + log.Printf("VendorID : %x\n", d.VendorID) + log.Printf("ProductID : %x\n", d.ProductID) + log.Printf("Release : %x\n", d.Release) + log.Printf("Serial : %x\n", d.Serial) + log.Printf("Manufacturer : %s\n", d.Manufacturer) + log.Printf("Product : %s\n", d.Product) + log.Printf("UsagePage : %x\n", d.UsagePage) + log.Printf("Usage : %x\n", d.Usage) + log.Printf("\n") +} + func isLedgerDevice(d hid.DeviceInfo) bool { deviceFound := d.UsagePage == UsagePageLedgerNanoS // Workarounds for possible empty usage pages diff --git a/ledger_mock.go b/ledger_mock.go index a16eb10..e9057fc 100644 --- a/ledger_mock.go +++ b/ledger_mock.go @@ -24,45 +24,52 @@ import ( "fmt" ) +const mockDeviceName = "Mock device" + type LedgerAdminMock struct{} type LedgerDeviceMock struct { - commands map[string][]byte + commands map[string]string } -func NewLedgerAdmin() *LedgerAdminMock { +func NewLedgerAdmin() LedgerAdmin { return &LedgerAdminMock{} } func (admin *LedgerAdminMock) ListDevices() ([]string, error) { - x := []string{"Mock device"} - return x, nil + return []string{mockDeviceName}, nil } func (admin *LedgerAdminMock) CountDevices() int { return 1 } -func (admin *LedgerAdminMock) Connect(deviceIndex int) (*LedgerDeviceMock, error) { +func (admin *LedgerAdminMock) Connect(deviceIndex int) (LedgerDevice, error) { return NewLedgerDeviceMock(), nil } func NewLedgerDeviceMock() *LedgerDeviceMock { return &LedgerDeviceMock{ - commands: map[string][]byte{ - "e001000000": []byte{0x31, 0x10, 0x00, 0x04, 0x08, 0x53, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x6f, 0x73, 0x00, 0x0b, 0x53, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x6f, 0x73, 0x4d, 0x43, 0x55}, - }, + commands: make(map[string]string), } } func (ledger *LedgerDeviceMock) Exchange(command []byte) ([]byte, error) { hexCommand := hex.EncodeToString(command) if reply, ok := ledger.commands[hexCommand]; ok { - return reply, nil + return hex.DecodeString(reply) } return nil, fmt.Errorf("unknown command: %s", hexCommand) } +func (ledger *LedgerDeviceMock) SetCommandReplies(commands map[string]string) { + ledger.commands = commands +} + +func (ledger *LedgerDeviceMock) ClearCommands() { + ledger.commands = make(map[string]string) +} + func (ledger *LedgerDeviceMock) Close() error { // Nothing to do here return nil diff --git a/ledger_test.go b/ledger_test.go index a117864..82060df 100644 --- a/ledger_test.go +++ b/ledger_test.go @@ -1,3 +1,6 @@ +//go:build ledger_mock +// +build ledger_mock + /******************************************************************************* * (c) Zondax AG * @@ -27,13 +30,31 @@ import ( var mux sync.Mutex +func TestLedger(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + {"CountLedgerDevices", Test_CountLedgerDevices}, + {"ListDevices", TestListDevices}, + {"GetLedger", Test_GetLedger}, + {"BasicExchange", Test_BasicExchange}, + {"Connect", TestConnect}, + {"GetVersion", TestGetVersion}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func Test_CountLedgerDevices(t *testing.T) { mux.Lock() defer mux.Unlock() ledgerAdmin := NewLedgerAdmin() count := ledgerAdmin.CountDevices() - assert.True(t, count > 0) + require.True(t, count > 0) } func TestListDevices(t *testing.T) { @@ -78,6 +99,13 @@ func Test_BasicExchange(t *testing.T) { } defer ledger.Close() + // Set expected replies for the commands (only if using mock) + if mockLedger, ok := ledger.(*LedgerDeviceMock); ok { + mockLedger.SetCommandReplies(map[string]string{ + "e001000000": "311000040853706563756c6f73000b53706563756c6f734d4355", + }) + } + // Call device info (this should work in main menu and many apps) message := []byte{0xE0, 0x01, 0, 0, 0} @@ -114,11 +142,17 @@ func TestGetVersion(t *testing.T) { } defer ledger.Close() + // Set expected replies for the commands (only if using mock) + if mockLedger, ok := ledger.(*LedgerDeviceMock); ok { + mockLedger.SetCommandReplies(map[string]string{ + "e001000000": "311000040853706563756c6f73000b53706563756c6f734d4355", + }) + } + // Call device info (this should work in main menu and many apps) message := []byte{0xE0, 0x01, 0, 0, 0} response, err := ledger.Exchange(message) assert.NoError(t, err) assert.NotEmpty(t, response, "Response should not be empty") - -} \ No newline at end of file +} diff --git a/ledger_zemu.go b/ledger_zemu.go index 5a5183c..db8a026 100644 --- a/ledger_zemu.go +++ b/ledger_zemu.go @@ -27,6 +27,11 @@ import ( "google.golang.org/grpc" ) +const ( + defaultGrpcURL = "localhost" + defaultGrpcPort = "3002" +) + type LedgerAdminZemu struct { grpcURL string grpcPort string @@ -39,9 +44,8 @@ type LedgerDeviceZemu struct { func NewLedgerAdmin() *LedgerAdminZemu { return &LedgerAdminZemu{ - //TODO get this from flag value or from Zemu response - grpcURL: "localhost", - grpcPort: "3002", + grpcURL: defaultGrpcURL, + grpcPort: defaultGrpcPort, } }