controlclient_test.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package controlclient
  4. import (
  5. "errors"
  6. "fmt"
  7. "io"
  8. "reflect"
  9. "slices"
  10. "testing"
  11. "tailscale.com/types/netmap"
  12. "tailscale.com/types/persist"
  13. )
  14. func fieldsOf(t reflect.Type) (fields []string) {
  15. for i := range t.NumField() {
  16. if name := t.Field(i).Name; name != "_" {
  17. fields = append(fields, name)
  18. }
  19. }
  20. return
  21. }
  22. func TestStatusEqual(t *testing.T) {
  23. // Verify that the Equal method stays in sync with reality
  24. equalHandles := []string{"Err", "URL", "NetMap", "Persist", "state"}
  25. if have := fieldsOf(reflect.TypeFor[Status]()); !reflect.DeepEqual(have, equalHandles) {
  26. t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
  27. have, equalHandles)
  28. }
  29. tests := []struct {
  30. a, b *Status
  31. want bool
  32. }{
  33. {
  34. &Status{},
  35. nil,
  36. false,
  37. },
  38. {
  39. nil,
  40. &Status{},
  41. false,
  42. },
  43. {
  44. nil,
  45. nil,
  46. true,
  47. },
  48. {
  49. &Status{},
  50. &Status{},
  51. true,
  52. },
  53. {
  54. &Status{},
  55. &Status{state: StateAuthenticated},
  56. false,
  57. },
  58. }
  59. for i, tt := range tests {
  60. got := tt.a.Equal(tt.b)
  61. if got != tt.want {
  62. t.Errorf("%d. Equal = %v; want %v", i, got, tt.want)
  63. }
  64. }
  65. }
  66. // tests [canSkipStatus].
  67. func TestCanSkipStatus(t *testing.T) {
  68. st := new(Status)
  69. nm1 := &netmap.NetworkMap{}
  70. nm2 := &netmap.NetworkMap{}
  71. tests := []struct {
  72. name string
  73. s1, s2 *Status
  74. want bool
  75. }{
  76. {
  77. name: "nil-s2",
  78. s1: st,
  79. s2: nil,
  80. want: false,
  81. },
  82. {
  83. name: "equal",
  84. s1: st,
  85. s2: st,
  86. want: false,
  87. },
  88. {
  89. name: "s1-error",
  90. s1: &Status{Err: io.EOF, NetMap: nm1},
  91. s2: &Status{NetMap: nm2},
  92. want: false,
  93. },
  94. {
  95. name: "s1-url",
  96. s1: &Status{URL: "foo", NetMap: nm1},
  97. s2: &Status{NetMap: nm2},
  98. want: false,
  99. },
  100. {
  101. name: "s1-persist-diff",
  102. s1: &Status{Persist: new(persist.Persist).View(), NetMap: nm1},
  103. s2: &Status{NetMap: nm2},
  104. want: false,
  105. },
  106. {
  107. name: "s1-state-diff",
  108. s1: &Status{state: 123, NetMap: nm1},
  109. s2: &Status{NetMap: nm2},
  110. want: false,
  111. },
  112. {
  113. name: "s1-no-netmap1",
  114. s1: &Status{NetMap: nil},
  115. s2: &Status{NetMap: nm2},
  116. want: false,
  117. },
  118. {
  119. name: "s1-no-netmap2",
  120. s1: &Status{NetMap: nm1},
  121. s2: &Status{NetMap: nil},
  122. want: false,
  123. },
  124. {
  125. name: "skip",
  126. s1: &Status{NetMap: nm1},
  127. s2: &Status{NetMap: nm2},
  128. want: true,
  129. },
  130. }
  131. for _, tt := range tests {
  132. t.Run(tt.name, func(t *testing.T) {
  133. if got := canSkipStatus(tt.s1, tt.s2); got != tt.want {
  134. t.Errorf("canSkipStatus = %v, want %v", got, tt.want)
  135. }
  136. })
  137. }
  138. want := []string{"Err", "URL", "NetMap", "Persist", "state"}
  139. if f := fieldsOf(reflect.TypeFor[Status]()); !slices.Equal(f, want) {
  140. t.Errorf("Status fields = %q; this code was only written to handle fields %q", f, want)
  141. }
  142. }
  143. func TestRetryableErrors(t *testing.T) {
  144. errorTests := []struct {
  145. err error
  146. want bool
  147. }{
  148. {errNoNoiseClient, true},
  149. {errNoNodeKey, true},
  150. {fmt.Errorf("%w: %w", errNoNoiseClient, errors.New("no noise")), true},
  151. {fmt.Errorf("%w: %w", errHTTPPostFailure, errors.New("bad post")), true},
  152. {fmt.Errorf("%w: %w", errNoNodeKey, errors.New("not node key")), true},
  153. {errBadHTTPResponse(429, "too may requests"), true},
  154. {errBadHTTPResponse(500, "internal server eror"), true},
  155. {errBadHTTPResponse(502, "bad gateway"), true},
  156. {errBadHTTPResponse(503, "service unavailable"), true},
  157. {errBadHTTPResponse(504, "gateway timeout"), true},
  158. {errBadHTTPResponse(1234, "random error"), false},
  159. }
  160. for _, tt := range errorTests {
  161. t.Run(tt.err.Error(), func(t *testing.T) {
  162. if isRetryableErrorForTest(tt.err) != tt.want {
  163. t.Fatalf("retriable: got %v, want %v", tt.err, tt.want)
  164. }
  165. })
  166. }
  167. }
  168. type retryableForTest interface {
  169. Retryable() bool
  170. }
  171. func isRetryableErrorForTest(err error) bool {
  172. var ae retryableForTest
  173. if errors.As(err, &ae) {
  174. return ae.Retryable()
  175. }
  176. return false
  177. }