package api import ( "errors" "testing" ) // TestFrameRoundTrip: a frame survives encoding, with every field intact. func TestFrameRoundTrip(t *testing.T) { frame := Frame{ Kind: KindRequest, Method: MethodSync, Status: StatusOK, ID: 4294967295, Payload: []byte{1, 2, 3, 4}, } encoded, err := Encode(frame) if err != nil { t.Fatal(err) } if len(encoded) != HeaderLen+4 { t.Fatalf("the frame is %d bytes", len(encoded)) } decoded, err := Decode(encoded) if err != nil { t.Fatal(err) } if decoded.Kind != frame.Kind || decoded.Method != frame.Method || decoded.Status != frame.Status || decoded.ID != frame.ID || string(decoded.Payload) != string(frame.Payload) { t.Fatalf("decoded %+v", decoded) } } // TestBadFramesAreRefused: a frame that is not this conversation is refused before anything // is decoded from it. func TestBadFramesAreRefused(t *testing.T) { good, err := Encode(Frame{Kind: KindRequest, Method: MethodStatus, ID: 1}) if err != nil { t.Fatal(err) } cases := map[string][]byte{ "empty": nil, "short": good[:HeaderLen-1], "wrong magic": mutate(good, 0, 0x00), "wrong version": mutate(good, 4, 99), "lying length": mutate(good, 14, 0x08), "trailing": append(append([]byte{}, good...), 0xff), } huge := append([]byte{}, good...) huge[14], huge[15], huge[16], huge[17] = 0xff, 0xff, 0xff, 0x7f cases["huge length"] = huge for name, raw := range cases { if _, err := Decode(raw); !errors.Is(err, ErrBadFrame) { t.Fatalf("%s: %v", name, err) } } } // mutate copies a frame and replaces one byte, so a refusal is about that byte. func mutate(raw []byte, index int, value byte) []byte { out := append([]byte{}, raw...) out[index] = value return out } // TestOverlongPayloadIsRefused: a frame this version cannot carry is a bug, not a frame. func TestOverlongPayloadIsRefused(t *testing.T) { if _, err := Encode(Frame{Payload: make([]byte, MaxPayload+1)}); !errors.Is(err, ErrBadFrame) { t.Fatalf("an overlong payload was encoded: %v", err) } } // TestStatusRoundTrip is the message a page draws after every action. func TestStatusRoundTrip(t *testing.T) { status := ChainStatus{ Chain: "blake", Profile: "xbt", HRP: "bcrt", Account: 1, HasWallet: true, Unlocked: false, Tip: 120, Filters: 118, Running: false, Receive: 7, Change: 2, Matches: 3, } decoded, err := ParseChainStatus(status.Encode()) if err != nil { t.Fatal(err) } if decoded != status { t.Fatalf("decoded %+v", decoded) } } // TestSetupRoundTrip: the words are the one payload that must not be reshaped on the way out. func TestSetupRoundTrip(t *testing.T) { request := SetupRequest{ Chain: "btc", Words: 24, Passphrase: "correct horse battery staple", } decoded, err := ParseSetupRequest(MethodCreate, request.Encode()) if err != nil { t.Fatal(err) } if decoded != request { t.Fatalf("decoded %+v", decoded) } result := SetupResult{ Chain: "btc", Mnemonic: "abandon about", Address: "bcrt1qxyz", Path: "m/84'/1'/0'/0/0", } decodedResult, err := ParseSetupResult(result.Encode()) if err != nil { t.Fatal(err) } if decodedResult != result { t.Fatalf("decoded %+v", decodedResult) } } // TestAddressesRoundTrip: the three parallel lists stay parallel. func TestAddressesRoundTrip(t *testing.T) { result := AddressesResult{ Addresses: []string{"bcrt1qa", "bcrt1qb", "bcrt1qc"}, Indexes: []uint32{0, 1, 2}, Paths: []string{"m/84'/1'/0'/0/0", "m/84'/1'/0'/0/1", "m/84'/1'/0'/0/2"}, } decoded, err := ParseAddressesResult(result.Encode()) if err != nil { t.Fatal(err) } if len(decoded.Addresses) != 3 || decoded.Addresses[2] != "bcrt1qc" { t.Fatalf("addresses %v", decoded.Addresses) } if len(decoded.Indexes) != 3 || decoded.Indexes[1] != 1 { t.Fatalf("indexes %v", decoded.Indexes) } if decoded.Paths[0] != result.Paths[0] { t.Fatalf("paths %v", decoded.Paths) } } // TestSyncRoundTrip: a sync report is what a page shows as "found N". func TestSyncRoundTrip(t *testing.T) { result := SyncResult{ From: 121, Tip: 130, Filters: 130, Scripts: 202, Matched: []uint32{122, 129}, New: []uint32{129}, } decoded, err := ParseSyncResult(result.Encode()) if err != nil { t.Fatal(err) } if decoded.From != 121 || decoded.Tip != 130 || decoded.Scripts != 202 { t.Fatalf("decoded %+v", decoded) } if len(decoded.Matched) != 2 || decoded.Matched[1] != 129 || len(decoded.New) != 1 { t.Fatalf("decoded %+v", decoded) } } // TestEmptyLists: zero is a valid count, not a missing field. func TestEmptyLists(t *testing.T) { heights, err := ParseHeights(MethodMatches, Heights{}.Encode()) if err != nil { t.Fatal(err) } if heights.Heights == nil || len(heights.Heights) != 0 { t.Fatalf("heights %v", heights.Heights) } chains, err := ParseChains(Chains{}.Encode()) if err != nil { t.Fatal(err) } if len(chains.Names) != 0 { t.Fatalf("names %v", chains.Names) } } // TestTruncatedPayloadsAreRefused: every parser says no rather than reading past the end. func TestTruncatedPayloadsAreRefused(t *testing.T) { status := ChainStatus{Chain: "btc", Profile: "btc", HRP: "bc", Account: 0} full := status.Encode() for length := 0; length < len(full); length++ { if _, err := ParseChainStatus(full[:length]); err == nil { t.Fatalf("a %d-byte status parsed", length) } } request := AddressesRequest{Chain: "btc", Count: 3} encoded := request.Encode() for length := 0; length < len(encoded); length++ { if _, err := ParseAddressesRequest(encoded[:length]); err == nil { t.Fatalf("a %d-byte addresses request parsed", length) } } } // TestTrailingBytesAreRefused: a payload with leftovers is a version disagreement. func TestTrailingBytesAreRefused(t *testing.T) { encoded := append(ChainRequest{Chain: "btc"}.Encode(), 0xaa) if _, err := ParseChainRequest(encoded); !errors.Is(err, ErrBadFrame) { t.Fatalf("a trailing byte was accepted: %v", err) } sync := append(SyncRequest{Chain: "btc", Lookahead: 5}.Encode(), 0xaa) if _, err := ParseSyncRequest(sync); !errors.Is(err, ErrBadFrame) { t.Fatalf("a trailing byte was accepted: %v", err) } } // TestMessageIsAResult: the helper every refusal uses builds a result frame, not a request. func TestMessageIsAResult(t *testing.T) { frame := Message(StatusFailed, 7, "the wallet is locked") if frame.Kind != KindResult || frame.Status != StatusFailed || frame.ID != 7 { t.Fatalf("the frame is %+v", frame) } text, rest, err := ReadText(frame.Payload) if err != nil { t.Fatal(err) } if text != "the wallet is locked" || len(rest) != 0 { t.Fatalf("the text is %q", text) } } // TestKindAndMethodStrings: the names are what a log line carries. func TestKindAndMethodStrings(t *testing.T) { if KindRequest.String() != "request" || Kind(99).String() == "" { t.Fatal("kind names") } for _, method := range []Method{MethodStatus, MethodCreate, MethodRestore, MethodUnlock, MethodLock, MethodAddresses, MethodSync, MethodMatches, MethodChains} { if method.String() == "" || method.String()[0] == 'm' && method.String() != "matches" { t.Fatalf("the method %d has no name", method) } } if StatusFailed.String() != "failed" { t.Fatal("status names") } } // TestCoinListRoundTrip: coins are fixed-width records, and a mismatched count is a refusal. func TestCoinListRoundTrip(t *testing.T) { var first, second [32]byte first[0], second[0] = 1, 2 list := CoinList{Coins: []Coin{ {TxID: first, Vout: 0, Value: 50000, Height: 10}, {TxID: second, Vout: 3, Value: 70000, Height: 0}, }} decoded, err := ParseCoinList(list.Encode()) if err != nil { t.Fatal(err) } if len(decoded.Coins) != 2 || decoded.Coins[1].Vout != 3 || decoded.Coins[0].Value != 50000 { t.Fatalf("decoded %+v", decoded.Coins) } if decoded.Coins[0].TxID != first || decoded.Coins[1].TxID != second { t.Fatal("the ids did not survive") } if _, err := ParseCoinList(list.Encode()[:10]); err == nil { t.Fatal("a truncated coin list parsed") } if _, err := ParseCoinList(nil); err == nil { t.Fatal("an empty payload parsed as a list") } }