api_test.go raw
1 package api
2
3 import (
4 "errors"
5 "testing"
6 )
7
8 // TestFrameRoundTrip: a frame survives encoding, with every field intact.
9 func TestFrameRoundTrip(t *testing.T) {
10 frame := Frame{
11 Kind: KindRequest,
12 Method: MethodSync,
13 Status: StatusOK,
14 ID: 4294967295,
15 Payload: []byte{1, 2, 3, 4},
16 }
17
18 encoded, err := Encode(frame)
19
20 if err != nil {
21 t.Fatal(err)
22 }
23
24 if len(encoded) != HeaderLen+4 {
25 t.Fatalf("the frame is %d bytes", len(encoded))
26 }
27
28 decoded, err := Decode(encoded)
29
30 if err != nil {
31 t.Fatal(err)
32 }
33
34 if decoded.Kind != frame.Kind || decoded.Method != frame.Method || decoded.Status != frame.Status ||
35 decoded.ID != frame.ID || string(decoded.Payload) != string(frame.Payload) {
36 t.Fatalf("decoded %+v", decoded)
37 }
38 }
39
40 // TestBadFramesAreRefused: a frame that is not this conversation is refused before anything
41 // is decoded from it.
42 func TestBadFramesAreRefused(t *testing.T) {
43 good, err := Encode(Frame{Kind: KindRequest, Method: MethodStatus, ID: 1})
44
45 if err != nil {
46 t.Fatal(err)
47 }
48
49 cases := map[string][]byte{
50 "empty": nil,
51 "short": good[:HeaderLen-1],
52 "wrong magic": mutate(good, 0, 0x00),
53 "wrong version": mutate(good, 4, 99),
54 "lying length": mutate(good, 14, 0x08),
55 "trailing": append(append([]byte{}, good...), 0xff),
56 }
57
58 huge := append([]byte{}, good...)
59 huge[14], huge[15], huge[16], huge[17] = 0xff, 0xff, 0xff, 0x7f
60 cases["huge length"] = huge
61
62 for name, raw := range cases {
63 if _, err := Decode(raw); !errors.Is(err, ErrBadFrame) {
64 t.Fatalf("%s: %v", name, err)
65 }
66 }
67 }
68
69 // mutate copies a frame and replaces one byte, so a refusal is about that byte.
70 func mutate(raw []byte, index int, value byte) []byte {
71 out := append([]byte{}, raw...)
72 out[index] = value
73
74 return out
75 }
76
77 // TestOverlongPayloadIsRefused: a frame this version cannot carry is a bug, not a frame.
78 func TestOverlongPayloadIsRefused(t *testing.T) {
79 if _, err := Encode(Frame{Payload: make([]byte, MaxPayload+1)}); !errors.Is(err, ErrBadFrame) {
80 t.Fatalf("an overlong payload was encoded: %v", err)
81 }
82 }
83
84 // TestStatusRoundTrip is the message a page draws after every action.
85 func TestStatusRoundTrip(t *testing.T) {
86 status := ChainStatus{
87 Chain: "blake", Profile: "xbt", HRP: "bcrt", Account: 1,
88 HasWallet: true, Unlocked: false,
89 Tip: 120, Filters: 118, Running: false,
90 Receive: 7, Change: 2, Matches: 3,
91 }
92
93 decoded, err := ParseChainStatus(status.Encode())
94
95 if err != nil {
96 t.Fatal(err)
97 }
98
99 if decoded != status {
100 t.Fatalf("decoded %+v", decoded)
101 }
102 }
103
104 // TestSetupRoundTrip: the words are the one payload that must not be reshaped on the way out.
105 func TestSetupRoundTrip(t *testing.T) {
106 request := SetupRequest{
107 Chain: "btc", Words: 24, Passphrase: "correct horse battery staple",
108 }
109
110 decoded, err := ParseSetupRequest(MethodCreate, request.Encode())
111
112 if err != nil {
113 t.Fatal(err)
114 }
115
116 if decoded != request {
117 t.Fatalf("decoded %+v", decoded)
118 }
119
120 result := SetupResult{
121 Chain: "btc", Mnemonic: "abandon about", Address: "bcrt1qxyz", Path: "m/84'/1'/0'/0/0",
122 }
123
124 decodedResult, err := ParseSetupResult(result.Encode())
125
126 if err != nil {
127 t.Fatal(err)
128 }
129
130 if decodedResult != result {
131 t.Fatalf("decoded %+v", decodedResult)
132 }
133 }
134
135 // TestAddressesRoundTrip: the three parallel lists stay parallel.
136 func TestAddressesRoundTrip(t *testing.T) {
137 result := AddressesResult{
138 Addresses: []string{"bcrt1qa", "bcrt1qb", "bcrt1qc"},
139 Indexes: []uint32{0, 1, 2},
140 Paths: []string{"m/84'/1'/0'/0/0", "m/84'/1'/0'/0/1", "m/84'/1'/0'/0/2"},
141 }
142
143 decoded, err := ParseAddressesResult(result.Encode())
144
145 if err != nil {
146 t.Fatal(err)
147 }
148
149 if len(decoded.Addresses) != 3 || decoded.Addresses[2] != "bcrt1qc" {
150 t.Fatalf("addresses %v", decoded.Addresses)
151 }
152
153 if len(decoded.Indexes) != 3 || decoded.Indexes[1] != 1 {
154 t.Fatalf("indexes %v", decoded.Indexes)
155 }
156
157 if decoded.Paths[0] != result.Paths[0] {
158 t.Fatalf("paths %v", decoded.Paths)
159 }
160 }
161
162 // TestSyncRoundTrip: a sync report is what a page shows as "found N".
163 func TestSyncRoundTrip(t *testing.T) {
164 result := SyncResult{
165 From: 121, Tip: 130, Filters: 130, Scripts: 202,
166 Matched: []uint32{122, 129}, New: []uint32{129},
167 }
168
169 decoded, err := ParseSyncResult(result.Encode())
170
171 if err != nil {
172 t.Fatal(err)
173 }
174
175 if decoded.From != 121 || decoded.Tip != 130 || decoded.Scripts != 202 {
176 t.Fatalf("decoded %+v", decoded)
177 }
178
179 if len(decoded.Matched) != 2 || decoded.Matched[1] != 129 || len(decoded.New) != 1 {
180 t.Fatalf("decoded %+v", decoded)
181 }
182 }
183
184 // TestEmptyLists: zero is a valid count, not a missing field.
185 func TestEmptyLists(t *testing.T) {
186 heights, err := ParseHeights(MethodMatches, Heights{}.Encode())
187
188 if err != nil {
189 t.Fatal(err)
190 }
191
192 if heights.Heights == nil || len(heights.Heights) != 0 {
193 t.Fatalf("heights %v", heights.Heights)
194 }
195
196 chains, err := ParseChains(Chains{}.Encode())
197
198 if err != nil {
199 t.Fatal(err)
200 }
201
202 if len(chains.Names) != 0 {
203 t.Fatalf("names %v", chains.Names)
204 }
205 }
206
207 // TestTruncatedPayloadsAreRefused: every parser says no rather than reading past the end.
208 func TestTruncatedPayloadsAreRefused(t *testing.T) {
209 status := ChainStatus{Chain: "btc", Profile: "btc", HRP: "bc", Account: 0}
210 full := status.Encode()
211
212 for length := 0; length < len(full); length++ {
213 if _, err := ParseChainStatus(full[:length]); err == nil {
214 t.Fatalf("a %d-byte status parsed", length)
215 }
216 }
217
218 request := AddressesRequest{Chain: "btc", Count: 3}
219 encoded := request.Encode()
220
221 for length := 0; length < len(encoded); length++ {
222 if _, err := ParseAddressesRequest(encoded[:length]); err == nil {
223 t.Fatalf("a %d-byte addresses request parsed", length)
224 }
225 }
226 }
227
228 // TestTrailingBytesAreRefused: a payload with leftovers is a version disagreement.
229 func TestTrailingBytesAreRefused(t *testing.T) {
230 encoded := append(ChainRequest{Chain: "btc"}.Encode(), 0xaa)
231
232 if _, err := ParseChainRequest(encoded); !errors.Is(err, ErrBadFrame) {
233 t.Fatalf("a trailing byte was accepted: %v", err)
234 }
235
236 sync := append(SyncRequest{Chain: "btc", Lookahead: 5}.Encode(), 0xaa)
237
238 if _, err := ParseSyncRequest(sync); !errors.Is(err, ErrBadFrame) {
239 t.Fatalf("a trailing byte was accepted: %v", err)
240 }
241 }
242
243 // TestMessageIsAResult: the helper every refusal uses builds a result frame, not a request.
244 func TestMessageIsAResult(t *testing.T) {
245 frame := Message(StatusFailed, 7, "the wallet is locked")
246
247 if frame.Kind != KindResult || frame.Status != StatusFailed || frame.ID != 7 {
248 t.Fatalf("the frame is %+v", frame)
249 }
250
251 text, rest, err := ReadText(frame.Payload)
252
253 if err != nil {
254 t.Fatal(err)
255 }
256
257 if text != "the wallet is locked" || len(rest) != 0 {
258 t.Fatalf("the text is %q", text)
259 }
260 }
261
262 // TestKindAndMethodStrings: the names are what a log line carries.
263 func TestKindAndMethodStrings(t *testing.T) {
264 if KindRequest.String() != "request" || Kind(99).String() == "" {
265 t.Fatal("kind names")
266 }
267
268 for _, method := range []Method{MethodStatus, MethodCreate, MethodRestore, MethodUnlock,
269 MethodLock, MethodAddresses, MethodSync, MethodMatches, MethodChains} {
270 if method.String() == "" || method.String()[0] == 'm' && method.String() != "matches" {
271 t.Fatalf("the method %d has no name", method)
272 }
273 }
274
275 if StatusFailed.String() != "failed" {
276 t.Fatal("status names")
277 }
278 }
279
280 // TestCoinListRoundTrip: coins are fixed-width records, and a mismatched count is a refusal.
281 func TestCoinListRoundTrip(t *testing.T) {
282 var first, second [32]byte
283 first[0], second[0] = 1, 2
284
285 list := CoinList{Coins: []Coin{
286 {TxID: first, Vout: 0, Value: 50000, Height: 10},
287 {TxID: second, Vout: 3, Value: 70000, Height: 0},
288 }}
289
290 decoded, err := ParseCoinList(list.Encode())
291
292 if err != nil {
293 t.Fatal(err)
294 }
295
296 if len(decoded.Coins) != 2 || decoded.Coins[1].Vout != 3 || decoded.Coins[0].Value != 50000 {
297 t.Fatalf("decoded %+v", decoded.Coins)
298 }
299
300 if decoded.Coins[0].TxID != first || decoded.Coins[1].TxID != second {
301 t.Fatal("the ids did not survive")
302 }
303
304 if _, err := ParseCoinList(list.Encode()[:10]); err == nil {
305 t.Fatal("a truncated coin list parsed")
306 }
307
308 if _, err := ParseCoinList(nil); err == nil {
309 t.Fatal("an empty payload parsed as a list")
310 }
311 }
312