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