client_test.go 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962
  1. // Copyright 2015 CoreOS, Inc.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package etcdhttp
  15. import (
  16. "bytes"
  17. "encoding/json"
  18. "errors"
  19. "io/ioutil"
  20. "net/http"
  21. "net/http/httptest"
  22. "net/url"
  23. "path"
  24. "reflect"
  25. "strings"
  26. "testing"
  27. "time"
  28. "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-semver/semver"
  29. "github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork"
  30. "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
  31. etcdErr "github.com/coreos/etcd/error"
  32. "github.com/coreos/etcd/etcdserver"
  33. "github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
  34. "github.com/coreos/etcd/etcdserver/etcdserverpb"
  35. "github.com/coreos/etcd/pkg/testutil"
  36. "github.com/coreos/etcd/pkg/types"
  37. "github.com/coreos/etcd/raft/raftpb"
  38. "github.com/coreos/etcd/store"
  39. "github.com/coreos/etcd/version"
  40. )
  41. func mustMarshalEvent(t *testing.T, ev *store.Event) string {
  42. b := new(bytes.Buffer)
  43. if err := json.NewEncoder(b).Encode(ev); err != nil {
  44. t.Fatalf("error marshalling event %#v: %v", ev, err)
  45. }
  46. return b.String()
  47. }
  48. // mustNewForm takes a set of Values and constructs a PUT *http.Request,
  49. // with a URL constructed from appending the given path to the standard keysPrefix
  50. func mustNewForm(t *testing.T, p string, vals url.Values) *http.Request {
  51. u := testutil.MustNewURL(t, path.Join(keysPrefix, p))
  52. req, err := http.NewRequest("PUT", u.String(), strings.NewReader(vals.Encode()))
  53. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  54. if err != nil {
  55. t.Fatalf("error creating new request: %v", err)
  56. }
  57. return req
  58. }
  59. // mustNewPostForm takes a set of Values and constructs a POST *http.Request,
  60. // with a URL constructed from appending the given path to the standard keysPrefix
  61. func mustNewPostForm(t *testing.T, p string, vals url.Values) *http.Request {
  62. u := testutil.MustNewURL(t, path.Join(keysPrefix, p))
  63. req, err := http.NewRequest("POST", u.String(), strings.NewReader(vals.Encode()))
  64. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  65. if err != nil {
  66. t.Fatalf("error creating new request: %v", err)
  67. }
  68. return req
  69. }
  70. // mustNewRequest takes a path, appends it to the standard keysPrefix, and constructs
  71. // a GET *http.Request referencing the resulting URL
  72. func mustNewRequest(t *testing.T, p string) *http.Request {
  73. return mustNewMethodRequest(t, "GET", p)
  74. }
  75. func mustNewMethodRequest(t *testing.T, m, p string) *http.Request {
  76. return &http.Request{
  77. Method: m,
  78. URL: testutil.MustNewURL(t, path.Join(keysPrefix, p)),
  79. }
  80. }
  81. type serverRecorder struct {
  82. actions []action
  83. }
  84. func (s *serverRecorder) Start() {}
  85. func (s *serverRecorder) Stop() {}
  86. func (s *serverRecorder) Leader() types.ID { return types.ID(1) }
  87. func (s *serverRecorder) ID() types.ID { return types.ID(1) }
  88. func (s *serverRecorder) Do(_ context.Context, r etcdserverpb.Request) (etcdserver.Response, error) {
  89. s.actions = append(s.actions, action{name: "Do", params: []interface{}{r}})
  90. return etcdserver.Response{}, nil
  91. }
  92. func (s *serverRecorder) Process(_ context.Context, m raftpb.Message) error {
  93. s.actions = append(s.actions, action{name: "Process", params: []interface{}{m}})
  94. return nil
  95. }
  96. func (s *serverRecorder) AddMember(_ context.Context, m etcdserver.Member) error {
  97. s.actions = append(s.actions, action{name: "AddMember", params: []interface{}{m}})
  98. return nil
  99. }
  100. func (s *serverRecorder) RemoveMember(_ context.Context, id uint64) error {
  101. s.actions = append(s.actions, action{name: "RemoveMember", params: []interface{}{id}})
  102. return nil
  103. }
  104. func (s *serverRecorder) UpdateMember(_ context.Context, m etcdserver.Member) error {
  105. s.actions = append(s.actions, action{name: "UpdateMember", params: []interface{}{m}})
  106. return nil
  107. }
  108. func (s *serverRecorder) ClusterVersion() *semver.Version { return nil }
  109. type action struct {
  110. name string
  111. params []interface{}
  112. }
  113. // flushingRecorder provides a channel to allow users to block until the Recorder is Flushed()
  114. type flushingRecorder struct {
  115. *httptest.ResponseRecorder
  116. ch chan struct{}
  117. }
  118. func (fr *flushingRecorder) Flush() {
  119. fr.ResponseRecorder.Flush()
  120. fr.ch <- struct{}{}
  121. }
  122. // resServer implements the etcd.Server interface for testing.
  123. // It returns the given responsefrom any Do calls, and nil error
  124. type resServer struct {
  125. res etcdserver.Response
  126. }
  127. func (rs *resServer) Start() {}
  128. func (rs *resServer) Stop() {}
  129. func (rs *resServer) ID() types.ID { return types.ID(1) }
  130. func (rs *resServer) Leader() types.ID { return types.ID(1) }
  131. func (rs *resServer) Do(_ context.Context, _ etcdserverpb.Request) (etcdserver.Response, error) {
  132. return rs.res, nil
  133. }
  134. func (rs *resServer) Process(_ context.Context, _ raftpb.Message) error { return nil }
  135. func (rs *resServer) AddMember(_ context.Context, _ etcdserver.Member) error { return nil }
  136. func (rs *resServer) RemoveMember(_ context.Context, _ uint64) error { return nil }
  137. func (rs *resServer) UpdateMember(_ context.Context, _ etcdserver.Member) error { return nil }
  138. func (rs *resServer) ClusterVersion() *semver.Version { return nil }
  139. func boolp(b bool) *bool { return &b }
  140. type dummyRaftTimer struct{}
  141. func (drt dummyRaftTimer) Index() uint64 { return uint64(100) }
  142. func (drt dummyRaftTimer) Term() uint64 { return uint64(5) }
  143. type dummyWatcher struct {
  144. echan chan *store.Event
  145. sidx uint64
  146. }
  147. func (w *dummyWatcher) EventChan() chan *store.Event {
  148. return w.echan
  149. }
  150. func (w *dummyWatcher) StartIndex() uint64 { return w.sidx }
  151. func (w *dummyWatcher) Remove() {}
  152. func TestBadParseRequest(t *testing.T) {
  153. tests := []struct {
  154. in *http.Request
  155. wcode int
  156. }{
  157. {
  158. // parseForm failure
  159. &http.Request{
  160. Body: nil,
  161. Method: "PUT",
  162. },
  163. etcdErr.EcodeInvalidForm,
  164. },
  165. {
  166. // bad key prefix
  167. &http.Request{
  168. URL: testutil.MustNewURL(t, "/badprefix/"),
  169. },
  170. etcdErr.EcodeInvalidForm,
  171. },
  172. // bad values for prevIndex, waitIndex, ttl
  173. {
  174. mustNewForm(t, "foo", url.Values{"prevIndex": []string{"garbage"}}),
  175. etcdErr.EcodeIndexNaN,
  176. },
  177. {
  178. mustNewForm(t, "foo", url.Values{"prevIndex": []string{"1.5"}}),
  179. etcdErr.EcodeIndexNaN,
  180. },
  181. {
  182. mustNewForm(t, "foo", url.Values{"prevIndex": []string{"-1"}}),
  183. etcdErr.EcodeIndexNaN,
  184. },
  185. {
  186. mustNewForm(t, "foo", url.Values{"waitIndex": []string{"garbage"}}),
  187. etcdErr.EcodeIndexNaN,
  188. },
  189. {
  190. mustNewForm(t, "foo", url.Values{"waitIndex": []string{"??"}}),
  191. etcdErr.EcodeIndexNaN,
  192. },
  193. {
  194. mustNewForm(t, "foo", url.Values{"ttl": []string{"-1"}}),
  195. etcdErr.EcodeTTLNaN,
  196. },
  197. // bad values for recursive, sorted, wait, prevExist, dir, stream
  198. {
  199. mustNewForm(t, "foo", url.Values{"recursive": []string{"hahaha"}}),
  200. etcdErr.EcodeInvalidField,
  201. },
  202. {
  203. mustNewForm(t, "foo", url.Values{"recursive": []string{"1234"}}),
  204. etcdErr.EcodeInvalidField,
  205. },
  206. {
  207. mustNewForm(t, "foo", url.Values{"recursive": []string{"?"}}),
  208. etcdErr.EcodeInvalidField,
  209. },
  210. {
  211. mustNewForm(t, "foo", url.Values{"sorted": []string{"?"}}),
  212. etcdErr.EcodeInvalidField,
  213. },
  214. {
  215. mustNewForm(t, "foo", url.Values{"sorted": []string{"x"}}),
  216. etcdErr.EcodeInvalidField,
  217. },
  218. {
  219. mustNewForm(t, "foo", url.Values{"wait": []string{"?!"}}),
  220. etcdErr.EcodeInvalidField,
  221. },
  222. {
  223. mustNewForm(t, "foo", url.Values{"wait": []string{"yes"}}),
  224. etcdErr.EcodeInvalidField,
  225. },
  226. {
  227. mustNewForm(t, "foo", url.Values{"prevExist": []string{"yes"}}),
  228. etcdErr.EcodeInvalidField,
  229. },
  230. {
  231. mustNewForm(t, "foo", url.Values{"prevExist": []string{"#2"}}),
  232. etcdErr.EcodeInvalidField,
  233. },
  234. {
  235. mustNewForm(t, "foo", url.Values{"dir": []string{"no"}}),
  236. etcdErr.EcodeInvalidField,
  237. },
  238. {
  239. mustNewForm(t, "foo", url.Values{"dir": []string{"file"}}),
  240. etcdErr.EcodeInvalidField,
  241. },
  242. {
  243. mustNewForm(t, "foo", url.Values{"quorum": []string{"no"}}),
  244. etcdErr.EcodeInvalidField,
  245. },
  246. {
  247. mustNewForm(t, "foo", url.Values{"quorum": []string{"file"}}),
  248. etcdErr.EcodeInvalidField,
  249. },
  250. {
  251. mustNewForm(t, "foo", url.Values{"stream": []string{"zzz"}}),
  252. etcdErr.EcodeInvalidField,
  253. },
  254. {
  255. mustNewForm(t, "foo", url.Values{"stream": []string{"something"}}),
  256. etcdErr.EcodeInvalidField,
  257. },
  258. // prevValue cannot be empty
  259. {
  260. mustNewForm(t, "foo", url.Values{"prevValue": []string{""}}),
  261. etcdErr.EcodePrevValueRequired,
  262. },
  263. // wait is only valid with GET requests
  264. {
  265. mustNewMethodRequest(t, "HEAD", "foo?wait=true"),
  266. etcdErr.EcodeInvalidField,
  267. },
  268. // query values are considered
  269. {
  270. mustNewRequest(t, "foo?prevExist=wrong"),
  271. etcdErr.EcodeInvalidField,
  272. },
  273. {
  274. mustNewRequest(t, "foo?ttl=wrong"),
  275. etcdErr.EcodeTTLNaN,
  276. },
  277. // but body takes precedence if both are specified
  278. {
  279. mustNewForm(
  280. t,
  281. "foo?ttl=12",
  282. url.Values{"ttl": []string{"garbage"}},
  283. ),
  284. etcdErr.EcodeTTLNaN,
  285. },
  286. {
  287. mustNewForm(
  288. t,
  289. "foo?prevExist=false",
  290. url.Values{"prevExist": []string{"yes"}},
  291. ),
  292. etcdErr.EcodeInvalidField,
  293. },
  294. }
  295. for i, tt := range tests {
  296. got, err := parseKeyRequest(tt.in, clockwork.NewFakeClock())
  297. if err == nil {
  298. t.Errorf("#%d: unexpected nil error!", i)
  299. continue
  300. }
  301. ee, ok := err.(*etcdErr.Error)
  302. if !ok {
  303. t.Errorf("#%d: err is not etcd.Error!", i)
  304. continue
  305. }
  306. if ee.ErrorCode != tt.wcode {
  307. t.Errorf("#%d: code=%d, want %v", i, ee.ErrorCode, tt.wcode)
  308. t.Logf("cause: %#v", ee.Cause)
  309. }
  310. if !reflect.DeepEqual(got, etcdserverpb.Request{}) {
  311. t.Errorf("#%d: unexpected non-empty Request: %#v", i, got)
  312. }
  313. }
  314. }
  315. func TestGoodParseRequest(t *testing.T) {
  316. fc := clockwork.NewFakeClock()
  317. fc.Advance(1111)
  318. tests := []struct {
  319. in *http.Request
  320. w etcdserverpb.Request
  321. }{
  322. {
  323. // good prefix, all other values default
  324. mustNewRequest(t, "foo"),
  325. etcdserverpb.Request{
  326. Method: "GET",
  327. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  328. },
  329. },
  330. {
  331. // value specified
  332. mustNewForm(
  333. t,
  334. "foo",
  335. url.Values{"value": []string{"some_value"}},
  336. ),
  337. etcdserverpb.Request{
  338. Method: "PUT",
  339. Val: "some_value",
  340. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  341. },
  342. },
  343. {
  344. // prevIndex specified
  345. mustNewForm(
  346. t,
  347. "foo",
  348. url.Values{"prevIndex": []string{"98765"}},
  349. ),
  350. etcdserverpb.Request{
  351. Method: "PUT",
  352. PrevIndex: 98765,
  353. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  354. },
  355. },
  356. {
  357. // recursive specified
  358. mustNewForm(
  359. t,
  360. "foo",
  361. url.Values{"recursive": []string{"true"}},
  362. ),
  363. etcdserverpb.Request{
  364. Method: "PUT",
  365. Recursive: true,
  366. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  367. },
  368. },
  369. {
  370. // sorted specified
  371. mustNewForm(
  372. t,
  373. "foo",
  374. url.Values{"sorted": []string{"true"}},
  375. ),
  376. etcdserverpb.Request{
  377. Method: "PUT",
  378. Sorted: true,
  379. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  380. },
  381. },
  382. {
  383. // quorum specified
  384. mustNewForm(
  385. t,
  386. "foo",
  387. url.Values{"quorum": []string{"true"}},
  388. ),
  389. etcdserverpb.Request{
  390. Method: "PUT",
  391. Quorum: true,
  392. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  393. },
  394. },
  395. {
  396. // wait specified
  397. mustNewRequest(t, "foo?wait=true"),
  398. etcdserverpb.Request{
  399. Method: "GET",
  400. Wait: true,
  401. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  402. },
  403. },
  404. {
  405. // empty TTL specified
  406. mustNewRequest(t, "foo?ttl="),
  407. etcdserverpb.Request{
  408. Method: "GET",
  409. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  410. Expiration: 0,
  411. },
  412. },
  413. {
  414. // non-empty TTL specified
  415. mustNewRequest(t, "foo?ttl=5678"),
  416. etcdserverpb.Request{
  417. Method: "GET",
  418. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  419. Expiration: fc.Now().Add(5678 * time.Second).UnixNano(),
  420. },
  421. },
  422. {
  423. // zero TTL specified
  424. mustNewRequest(t, "foo?ttl=0"),
  425. etcdserverpb.Request{
  426. Method: "GET",
  427. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  428. Expiration: fc.Now().UnixNano(),
  429. },
  430. },
  431. {
  432. // dir specified
  433. mustNewRequest(t, "foo?dir=true"),
  434. etcdserverpb.Request{
  435. Method: "GET",
  436. Dir: true,
  437. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  438. },
  439. },
  440. {
  441. // dir specified negatively
  442. mustNewRequest(t, "foo?dir=false"),
  443. etcdserverpb.Request{
  444. Method: "GET",
  445. Dir: false,
  446. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  447. },
  448. },
  449. {
  450. // prevExist should be non-null if specified
  451. mustNewForm(
  452. t,
  453. "foo",
  454. url.Values{"prevExist": []string{"true"}},
  455. ),
  456. etcdserverpb.Request{
  457. Method: "PUT",
  458. PrevExist: boolp(true),
  459. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  460. },
  461. },
  462. {
  463. // prevExist should be non-null if specified
  464. mustNewForm(
  465. t,
  466. "foo",
  467. url.Values{"prevExist": []string{"false"}},
  468. ),
  469. etcdserverpb.Request{
  470. Method: "PUT",
  471. PrevExist: boolp(false),
  472. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  473. },
  474. },
  475. // mix various fields
  476. {
  477. mustNewForm(
  478. t,
  479. "foo",
  480. url.Values{
  481. "value": []string{"some value"},
  482. "prevExist": []string{"true"},
  483. "prevValue": []string{"previous value"},
  484. },
  485. ),
  486. etcdserverpb.Request{
  487. Method: "PUT",
  488. PrevExist: boolp(true),
  489. PrevValue: "previous value",
  490. Val: "some value",
  491. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  492. },
  493. },
  494. // query parameters should be used if given
  495. {
  496. mustNewForm(
  497. t,
  498. "foo?prevValue=woof",
  499. url.Values{},
  500. ),
  501. etcdserverpb.Request{
  502. Method: "PUT",
  503. PrevValue: "woof",
  504. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  505. },
  506. },
  507. // but form values should take precedence over query parameters
  508. {
  509. mustNewForm(
  510. t,
  511. "foo?prevValue=woof",
  512. url.Values{
  513. "prevValue": []string{"miaow"},
  514. },
  515. ),
  516. etcdserverpb.Request{
  517. Method: "PUT",
  518. PrevValue: "miaow",
  519. Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
  520. },
  521. },
  522. }
  523. for i, tt := range tests {
  524. got, err := parseKeyRequest(tt.in, fc)
  525. if err != nil {
  526. t.Errorf("#%d: err = %v, want %v", i, err, nil)
  527. }
  528. if !reflect.DeepEqual(got, tt.w) {
  529. t.Errorf("#%d: request=%#v, want %#v", i, got, tt.w)
  530. }
  531. }
  532. }
  533. func TestServeMembers(t *testing.T) {
  534. memb1 := etcdserver.Member{ID: 12, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8080"}}}
  535. memb2 := etcdserver.Member{ID: 13, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8081"}}}
  536. cluster := &fakeCluster{
  537. id: 1,
  538. members: map[uint64]*etcdserver.Member{1: &memb1, 2: &memb2},
  539. }
  540. h := &membersHandler{
  541. server: &serverRecorder{},
  542. clock: clockwork.NewFakeClock(),
  543. clusterInfo: cluster,
  544. }
  545. wmc := string(`{"members":[{"id":"c","name":"","peerURLs":[],"clientURLs":["http://localhost:8080"]},{"id":"d","name":"","peerURLs":[],"clientURLs":["http://localhost:8081"]}]}`)
  546. tests := []struct {
  547. path string
  548. wcode int
  549. wct string
  550. wbody string
  551. }{
  552. {membersPrefix, http.StatusOK, "application/json", wmc + "\n"},
  553. {membersPrefix + "/", http.StatusOK, "application/json", wmc + "\n"},
  554. {path.Join(membersPrefix, "100"), http.StatusNotFound, "application/json", `{"message":"Not found"}`},
  555. {path.Join(membersPrefix, "foobar"), http.StatusNotFound, "application/json", `{"message":"Not found"}`},
  556. }
  557. for i, tt := range tests {
  558. req, err := http.NewRequest("GET", testutil.MustNewURL(t, tt.path).String(), nil)
  559. if err != nil {
  560. t.Fatal(err)
  561. }
  562. rw := httptest.NewRecorder()
  563. h.ServeHTTP(rw, req)
  564. if rw.Code != tt.wcode {
  565. t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
  566. }
  567. if gct := rw.Header().Get("Content-Type"); gct != tt.wct {
  568. t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct)
  569. }
  570. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  571. wcid := cluster.ID().String()
  572. if gcid != wcid {
  573. t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
  574. }
  575. if rw.Body.String() != tt.wbody {
  576. t.Errorf("#%d: body = %q, want %q", i, rw.Body.String(), tt.wbody)
  577. }
  578. }
  579. }
  580. // TODO: consolidate **ALL** fake server implementations and add no leader test case.
  581. func TestServeLeader(t *testing.T) {
  582. memb1 := etcdserver.Member{ID: 1, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8080"}}}
  583. memb2 := etcdserver.Member{ID: 2, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8081"}}}
  584. cluster := &fakeCluster{
  585. id: 1,
  586. members: map[uint64]*etcdserver.Member{1: &memb1, 2: &memb2},
  587. }
  588. h := &membersHandler{
  589. server: &serverRecorder{},
  590. clock: clockwork.NewFakeClock(),
  591. clusterInfo: cluster,
  592. }
  593. wmc := string(`{"id":"1","name":"","peerURLs":[],"clientURLs":["http://localhost:8080"]}`)
  594. tests := []struct {
  595. path string
  596. wcode int
  597. wct string
  598. wbody string
  599. }{
  600. {membersPrefix + "leader", http.StatusOK, "application/json", wmc + "\n"},
  601. // TODO: add no leader case
  602. }
  603. for i, tt := range tests {
  604. req, err := http.NewRequest("GET", testutil.MustNewURL(t, tt.path).String(), nil)
  605. if err != nil {
  606. t.Fatal(err)
  607. }
  608. rw := httptest.NewRecorder()
  609. h.ServeHTTP(rw, req)
  610. if rw.Code != tt.wcode {
  611. t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
  612. }
  613. if gct := rw.Header().Get("Content-Type"); gct != tt.wct {
  614. t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct)
  615. }
  616. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  617. wcid := cluster.ID().String()
  618. if gcid != wcid {
  619. t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
  620. }
  621. if rw.Body.String() != tt.wbody {
  622. t.Errorf("#%d: body = %q, want %q", i, rw.Body.String(), tt.wbody)
  623. }
  624. }
  625. }
  626. func TestServeMembersCreate(t *testing.T) {
  627. u := testutil.MustNewURL(t, membersPrefix)
  628. b := []byte(`{"peerURLs":["http://127.0.0.1:1"]}`)
  629. req, err := http.NewRequest("POST", u.String(), bytes.NewReader(b))
  630. if err != nil {
  631. t.Fatal(err)
  632. }
  633. req.Header.Set("Content-Type", "application/json")
  634. s := &serverRecorder{}
  635. h := &membersHandler{
  636. server: s,
  637. clock: clockwork.NewFakeClock(),
  638. clusterInfo: &fakeCluster{id: 1},
  639. }
  640. rw := httptest.NewRecorder()
  641. h.ServeHTTP(rw, req)
  642. wcode := http.StatusCreated
  643. if rw.Code != wcode {
  644. t.Errorf("code=%d, want %d", rw.Code, wcode)
  645. }
  646. wct := "application/json"
  647. if gct := rw.Header().Get("Content-Type"); gct != wct {
  648. t.Errorf("content-type = %s, want %s", gct, wct)
  649. }
  650. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  651. wcid := h.clusterInfo.ID().String()
  652. if gcid != wcid {
  653. t.Errorf("cid = %s, want %s", gcid, wcid)
  654. }
  655. wb := `{"id":"2a86a83729b330d5","name":"","peerURLs":["http://127.0.0.1:1"],"clientURLs":[]}` + "\n"
  656. g := rw.Body.String()
  657. if g != wb {
  658. t.Errorf("got body=%q, want %q", g, wb)
  659. }
  660. wm := etcdserver.Member{
  661. ID: 3064321551348478165,
  662. RaftAttributes: etcdserver.RaftAttributes{
  663. PeerURLs: []string{"http://127.0.0.1:1"},
  664. },
  665. }
  666. wactions := []action{{name: "AddMember", params: []interface{}{wm}}}
  667. if !reflect.DeepEqual(s.actions, wactions) {
  668. t.Errorf("actions = %+v, want %+v", s.actions, wactions)
  669. }
  670. }
  671. func TestServeMembersDelete(t *testing.T) {
  672. req := &http.Request{
  673. Method: "DELETE",
  674. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "BEEF")),
  675. }
  676. s := &serverRecorder{}
  677. h := &membersHandler{
  678. server: s,
  679. clusterInfo: &fakeCluster{id: 1},
  680. }
  681. rw := httptest.NewRecorder()
  682. h.ServeHTTP(rw, req)
  683. wcode := http.StatusNoContent
  684. if rw.Code != wcode {
  685. t.Errorf("code=%d, want %d", rw.Code, wcode)
  686. }
  687. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  688. wcid := h.clusterInfo.ID().String()
  689. if gcid != wcid {
  690. t.Errorf("cid = %s, want %s", gcid, wcid)
  691. }
  692. g := rw.Body.String()
  693. if g != "" {
  694. t.Errorf("got body=%q, want %q", g, "")
  695. }
  696. wactions := []action{{name: "RemoveMember", params: []interface{}{uint64(0xBEEF)}}}
  697. if !reflect.DeepEqual(s.actions, wactions) {
  698. t.Errorf("actions = %+v, want %+v", s.actions, wactions)
  699. }
  700. }
  701. func TestServeMembersUpdate(t *testing.T) {
  702. u := testutil.MustNewURL(t, path.Join(membersPrefix, "1"))
  703. b := []byte(`{"peerURLs":["http://127.0.0.1:1"]}`)
  704. req, err := http.NewRequest("PUT", u.String(), bytes.NewReader(b))
  705. if err != nil {
  706. t.Fatal(err)
  707. }
  708. req.Header.Set("Content-Type", "application/json")
  709. s := &serverRecorder{}
  710. h := &membersHandler{
  711. server: s,
  712. clock: clockwork.NewFakeClock(),
  713. clusterInfo: &fakeCluster{id: 1},
  714. }
  715. rw := httptest.NewRecorder()
  716. h.ServeHTTP(rw, req)
  717. wcode := http.StatusNoContent
  718. if rw.Code != wcode {
  719. t.Errorf("code=%d, want %d", rw.Code, wcode)
  720. }
  721. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  722. wcid := h.clusterInfo.ID().String()
  723. if gcid != wcid {
  724. t.Errorf("cid = %s, want %s", gcid, wcid)
  725. }
  726. wm := etcdserver.Member{
  727. ID: 1,
  728. RaftAttributes: etcdserver.RaftAttributes{
  729. PeerURLs: []string{"http://127.0.0.1:1"},
  730. },
  731. }
  732. wactions := []action{{name: "UpdateMember", params: []interface{}{wm}}}
  733. if !reflect.DeepEqual(s.actions, wactions) {
  734. t.Errorf("actions = %+v, want %+v", s.actions, wactions)
  735. }
  736. }
  737. func TestServeMembersFail(t *testing.T) {
  738. tests := []struct {
  739. req *http.Request
  740. server etcdserver.Server
  741. wcode int
  742. }{
  743. {
  744. // bad method
  745. &http.Request{
  746. Method: "CONNECT",
  747. },
  748. &resServer{},
  749. http.StatusMethodNotAllowed,
  750. },
  751. {
  752. // bad method
  753. &http.Request{
  754. Method: "TRACE",
  755. },
  756. &resServer{},
  757. http.StatusMethodNotAllowed,
  758. },
  759. {
  760. // parse body error
  761. &http.Request{
  762. URL: testutil.MustNewURL(t, membersPrefix),
  763. Method: "POST",
  764. Body: ioutil.NopCloser(strings.NewReader("bad json")),
  765. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  766. },
  767. &resServer{},
  768. http.StatusBadRequest,
  769. },
  770. {
  771. // bad content type
  772. &http.Request{
  773. URL: testutil.MustNewURL(t, membersPrefix),
  774. Method: "POST",
  775. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
  776. Header: map[string][]string{"Content-Type": []string{"application/bad"}},
  777. },
  778. &errServer{},
  779. http.StatusUnsupportedMediaType,
  780. },
  781. {
  782. // bad url
  783. &http.Request{
  784. URL: testutil.MustNewURL(t, membersPrefix),
  785. Method: "POST",
  786. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://a"]}`)),
  787. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  788. },
  789. &errServer{},
  790. http.StatusBadRequest,
  791. },
  792. {
  793. // etcdserver.AddMember error
  794. &http.Request{
  795. URL: testutil.MustNewURL(t, membersPrefix),
  796. Method: "POST",
  797. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
  798. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  799. },
  800. &errServer{
  801. errors.New("Error while adding a member"),
  802. },
  803. http.StatusInternalServerError,
  804. },
  805. {
  806. // etcdserver.AddMember error
  807. &http.Request{
  808. URL: testutil.MustNewURL(t, membersPrefix),
  809. Method: "POST",
  810. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
  811. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  812. },
  813. &errServer{
  814. etcdserver.ErrIDExists,
  815. },
  816. http.StatusConflict,
  817. },
  818. {
  819. // etcdserver.AddMember error
  820. &http.Request{
  821. URL: testutil.MustNewURL(t, membersPrefix),
  822. Method: "POST",
  823. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
  824. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  825. },
  826. &errServer{
  827. etcdserver.ErrPeerURLexists,
  828. },
  829. http.StatusConflict,
  830. },
  831. {
  832. // etcdserver.RemoveMember error with arbitrary server error
  833. &http.Request{
  834. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "1")),
  835. Method: "DELETE",
  836. },
  837. &errServer{
  838. errors.New("Error while removing member"),
  839. },
  840. http.StatusInternalServerError,
  841. },
  842. {
  843. // etcdserver.RemoveMember error with previously removed ID
  844. &http.Request{
  845. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
  846. Method: "DELETE",
  847. },
  848. &errServer{
  849. etcdserver.ErrIDRemoved,
  850. },
  851. http.StatusGone,
  852. },
  853. {
  854. // etcdserver.RemoveMember error with nonexistent ID
  855. &http.Request{
  856. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
  857. Method: "DELETE",
  858. },
  859. &errServer{
  860. etcdserver.ErrIDNotFound,
  861. },
  862. http.StatusNotFound,
  863. },
  864. {
  865. // etcdserver.RemoveMember error with badly formed ID
  866. &http.Request{
  867. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "bad_id")),
  868. Method: "DELETE",
  869. },
  870. nil,
  871. http.StatusNotFound,
  872. },
  873. {
  874. // etcdserver.RemoveMember with no ID
  875. &http.Request{
  876. URL: testutil.MustNewURL(t, membersPrefix),
  877. Method: "DELETE",
  878. },
  879. nil,
  880. http.StatusMethodNotAllowed,
  881. },
  882. {
  883. // parse body error
  884. &http.Request{
  885. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
  886. Method: "PUT",
  887. Body: ioutil.NopCloser(strings.NewReader("bad json")),
  888. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  889. },
  890. &resServer{},
  891. http.StatusBadRequest,
  892. },
  893. {
  894. // bad content type
  895. &http.Request{
  896. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
  897. Method: "PUT",
  898. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
  899. Header: map[string][]string{"Content-Type": []string{"application/bad"}},
  900. },
  901. &errServer{},
  902. http.StatusUnsupportedMediaType,
  903. },
  904. {
  905. // bad url
  906. &http.Request{
  907. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
  908. Method: "PUT",
  909. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://a"]}`)),
  910. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  911. },
  912. &errServer{},
  913. http.StatusBadRequest,
  914. },
  915. {
  916. // etcdserver.UpdateMember error
  917. &http.Request{
  918. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
  919. Method: "PUT",
  920. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
  921. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  922. },
  923. &errServer{
  924. errors.New("blah"),
  925. },
  926. http.StatusInternalServerError,
  927. },
  928. {
  929. // etcdserver.UpdateMember error
  930. &http.Request{
  931. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
  932. Method: "PUT",
  933. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
  934. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  935. },
  936. &errServer{
  937. etcdserver.ErrPeerURLexists,
  938. },
  939. http.StatusConflict,
  940. },
  941. {
  942. // etcdserver.UpdateMember error
  943. &http.Request{
  944. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
  945. Method: "PUT",
  946. Body: ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
  947. Header: map[string][]string{"Content-Type": []string{"application/json"}},
  948. },
  949. &errServer{
  950. etcdserver.ErrIDNotFound,
  951. },
  952. http.StatusNotFound,
  953. },
  954. {
  955. // etcdserver.UpdateMember error with badly formed ID
  956. &http.Request{
  957. URL: testutil.MustNewURL(t, path.Join(membersPrefix, "bad_id")),
  958. Method: "PUT",
  959. },
  960. nil,
  961. http.StatusNotFound,
  962. },
  963. {
  964. // etcdserver.UpdateMember with no ID
  965. &http.Request{
  966. URL: testutil.MustNewURL(t, membersPrefix),
  967. Method: "PUT",
  968. },
  969. nil,
  970. http.StatusMethodNotAllowed,
  971. },
  972. }
  973. for i, tt := range tests {
  974. h := &membersHandler{
  975. server: tt.server,
  976. clusterInfo: &fakeCluster{id: 1},
  977. clock: clockwork.NewFakeClock(),
  978. }
  979. rw := httptest.NewRecorder()
  980. h.ServeHTTP(rw, tt.req)
  981. if rw.Code != tt.wcode {
  982. t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
  983. }
  984. if rw.Code != http.StatusMethodNotAllowed {
  985. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  986. wcid := h.clusterInfo.ID().String()
  987. if gcid != wcid {
  988. t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
  989. }
  990. }
  991. }
  992. }
  993. func TestWriteEvent(t *testing.T) {
  994. // nil event should not panic
  995. rec := httptest.NewRecorder()
  996. writeKeyEvent(rec, nil, dummyRaftTimer{})
  997. h := rec.Header()
  998. if len(h) > 0 {
  999. t.Fatalf("unexpected non-empty headers: %#v", h)
  1000. }
  1001. b := rec.Body.String()
  1002. if len(b) > 0 {
  1003. t.Fatalf("unexpected non-empty body: %q", b)
  1004. }
  1005. tests := []struct {
  1006. ev *store.Event
  1007. idx string
  1008. // TODO(jonboulle): check body as well as just status code
  1009. code int
  1010. err error
  1011. }{
  1012. // standard case, standard 200 response
  1013. {
  1014. &store.Event{
  1015. Action: store.Get,
  1016. Node: &store.NodeExtern{},
  1017. PrevNode: &store.NodeExtern{},
  1018. },
  1019. "0",
  1020. http.StatusOK,
  1021. nil,
  1022. },
  1023. // check new nodes return StatusCreated
  1024. {
  1025. &store.Event{
  1026. Action: store.Create,
  1027. Node: &store.NodeExtern{},
  1028. PrevNode: &store.NodeExtern{},
  1029. },
  1030. "0",
  1031. http.StatusCreated,
  1032. nil,
  1033. },
  1034. }
  1035. for i, tt := range tests {
  1036. rw := httptest.NewRecorder()
  1037. writeKeyEvent(rw, tt.ev, dummyRaftTimer{})
  1038. if gct := rw.Header().Get("Content-Type"); gct != "application/json" {
  1039. t.Errorf("case %d: bad Content-Type: got %q, want application/json", i, gct)
  1040. }
  1041. if gri := rw.Header().Get("X-Raft-Index"); gri != "100" {
  1042. t.Errorf("case %d: bad X-Raft-Index header: got %s, want %s", i, gri, "100")
  1043. }
  1044. if grt := rw.Header().Get("X-Raft-Term"); grt != "5" {
  1045. t.Errorf("case %d: bad X-Raft-Term header: got %s, want %s", i, grt, "5")
  1046. }
  1047. if gei := rw.Header().Get("X-Etcd-Index"); gei != tt.idx {
  1048. t.Errorf("case %d: bad X-Etcd-Index header: got %s, want %s", i, gei, tt.idx)
  1049. }
  1050. if rw.Code != tt.code {
  1051. t.Errorf("case %d: bad response code: got %d, want %v", i, rw.Code, tt.code)
  1052. }
  1053. }
  1054. }
  1055. func TestV2DeprecatedMachinesEndpoint(t *testing.T) {
  1056. tests := []struct {
  1057. method string
  1058. wcode int
  1059. }{
  1060. {"GET", http.StatusOK},
  1061. {"HEAD", http.StatusOK},
  1062. {"POST", http.StatusMethodNotAllowed},
  1063. }
  1064. m := &deprecatedMachinesHandler{clusterInfo: &etcdserver.Cluster{}}
  1065. s := httptest.NewServer(m)
  1066. defer s.Close()
  1067. for _, tt := range tests {
  1068. req, err := http.NewRequest(tt.method, s.URL+deprecatedMachinesPrefix, nil)
  1069. if err != nil {
  1070. t.Fatal(err)
  1071. }
  1072. resp, err := http.DefaultClient.Do(req)
  1073. if err != nil {
  1074. t.Fatal(err)
  1075. }
  1076. if resp.StatusCode != tt.wcode {
  1077. t.Errorf("StatusCode = %d, expected %d", resp.StatusCode, tt.wcode)
  1078. }
  1079. }
  1080. }
  1081. func TestServeMachines(t *testing.T) {
  1082. cluster := &fakeCluster{
  1083. clientURLs: []string{"http://localhost:8080", "http://localhost:8081", "http://localhost:8082"},
  1084. }
  1085. writer := httptest.NewRecorder()
  1086. req, err := http.NewRequest("GET", "", nil)
  1087. if err != nil {
  1088. t.Fatal(err)
  1089. }
  1090. h := &deprecatedMachinesHandler{clusterInfo: cluster}
  1091. h.ServeHTTP(writer, req)
  1092. w := "http://localhost:8080, http://localhost:8081, http://localhost:8082"
  1093. if g := writer.Body.String(); g != w {
  1094. t.Errorf("body = %s, want %s", g, w)
  1095. }
  1096. if writer.Code != http.StatusOK {
  1097. t.Errorf("code = %d, want %d", writer.Code, http.StatusOK)
  1098. }
  1099. }
  1100. func TestGetID(t *testing.T) {
  1101. tests := []struct {
  1102. path string
  1103. wok bool
  1104. wid types.ID
  1105. wcode int
  1106. }{
  1107. {
  1108. "123",
  1109. true, 0x123, http.StatusOK,
  1110. },
  1111. {
  1112. "bad_id",
  1113. false, 0, http.StatusNotFound,
  1114. },
  1115. {
  1116. "",
  1117. false, 0, http.StatusMethodNotAllowed,
  1118. },
  1119. }
  1120. for i, tt := range tests {
  1121. w := httptest.NewRecorder()
  1122. id, ok := getID(tt.path, w)
  1123. if id != tt.wid {
  1124. t.Errorf("#%d: id = %d, want %d", i, id, tt.wid)
  1125. }
  1126. if ok != tt.wok {
  1127. t.Errorf("#%d: ok = %t, want %t", i, ok, tt.wok)
  1128. }
  1129. if w.Code != tt.wcode {
  1130. t.Errorf("#%d code = %d, want %d", i, w.Code, tt.wcode)
  1131. }
  1132. }
  1133. }
  1134. type dummyStats struct {
  1135. data []byte
  1136. }
  1137. func (ds *dummyStats) SelfStats() []byte { return ds.data }
  1138. func (ds *dummyStats) LeaderStats() []byte { return ds.data }
  1139. func (ds *dummyStats) StoreStats() []byte { return ds.data }
  1140. func (ds *dummyStats) UpdateRecvApp(_ types.ID, _ int64) {}
  1141. func TestServeSelfStats(t *testing.T) {
  1142. wb := []byte("some statistics")
  1143. w := string(wb)
  1144. sh := &statsHandler{
  1145. stats: &dummyStats{data: wb},
  1146. }
  1147. rw := httptest.NewRecorder()
  1148. sh.serveSelf(rw, &http.Request{Method: "GET"})
  1149. if rw.Code != http.StatusOK {
  1150. t.Errorf("code = %d, want %d", rw.Code, http.StatusOK)
  1151. }
  1152. wct := "application/json"
  1153. if gct := rw.Header().Get("Content-Type"); gct != wct {
  1154. t.Errorf("Content-Type = %q, want %q", gct, wct)
  1155. }
  1156. if g := rw.Body.String(); g != w {
  1157. t.Errorf("body = %s, want %s", g, w)
  1158. }
  1159. }
  1160. func TestSelfServeStatsBad(t *testing.T) {
  1161. for _, m := range []string{"PUT", "POST", "DELETE"} {
  1162. sh := &statsHandler{}
  1163. rw := httptest.NewRecorder()
  1164. sh.serveSelf(
  1165. rw,
  1166. &http.Request{
  1167. Method: m,
  1168. },
  1169. )
  1170. if rw.Code != http.StatusMethodNotAllowed {
  1171. t.Errorf("method %s: code=%d, want %d", m, rw.Code, http.StatusMethodNotAllowed)
  1172. }
  1173. }
  1174. }
  1175. func TestLeaderServeStatsBad(t *testing.T) {
  1176. for _, m := range []string{"PUT", "POST", "DELETE"} {
  1177. sh := &statsHandler{}
  1178. rw := httptest.NewRecorder()
  1179. sh.serveLeader(
  1180. rw,
  1181. &http.Request{
  1182. Method: m,
  1183. },
  1184. )
  1185. if rw.Code != http.StatusMethodNotAllowed {
  1186. t.Errorf("method %s: code=%d, want %d", m, rw.Code, http.StatusMethodNotAllowed)
  1187. }
  1188. }
  1189. }
  1190. func TestServeLeaderStats(t *testing.T) {
  1191. wb := []byte("some statistics")
  1192. w := string(wb)
  1193. sh := &statsHandler{
  1194. stats: &dummyStats{data: wb},
  1195. }
  1196. rw := httptest.NewRecorder()
  1197. sh.serveLeader(rw, &http.Request{Method: "GET"})
  1198. if rw.Code != http.StatusOK {
  1199. t.Errorf("code = %d, want %d", rw.Code, http.StatusOK)
  1200. }
  1201. wct := "application/json"
  1202. if gct := rw.Header().Get("Content-Type"); gct != wct {
  1203. t.Errorf("Content-Type = %q, want %q", gct, wct)
  1204. }
  1205. if g := rw.Body.String(); g != w {
  1206. t.Errorf("body = %s, want %s", g, w)
  1207. }
  1208. }
  1209. func TestServeStoreStats(t *testing.T) {
  1210. wb := []byte("some statistics")
  1211. w := string(wb)
  1212. sh := &statsHandler{
  1213. stats: &dummyStats{data: wb},
  1214. }
  1215. rw := httptest.NewRecorder()
  1216. sh.serveStore(rw, &http.Request{Method: "GET"})
  1217. if rw.Code != http.StatusOK {
  1218. t.Errorf("code = %d, want %d", rw.Code, http.StatusOK)
  1219. }
  1220. wct := "application/json"
  1221. if gct := rw.Header().Get("Content-Type"); gct != wct {
  1222. t.Errorf("Content-Type = %q, want %q", gct, wct)
  1223. }
  1224. if g := rw.Body.String(); g != w {
  1225. t.Errorf("body = %s, want %s", g, w)
  1226. }
  1227. }
  1228. func TestServeVersion(t *testing.T) {
  1229. req, err := http.NewRequest("GET", "", nil)
  1230. if err != nil {
  1231. t.Fatalf("error creating request: %v", err)
  1232. }
  1233. rw := httptest.NewRecorder()
  1234. serveVersion(rw, req)
  1235. if rw.Code != http.StatusOK {
  1236. t.Errorf("code=%d, want %d", rw.Code, http.StatusOK)
  1237. }
  1238. w := version.MarshalJSON()
  1239. if g := rw.Body.String(); g != string(w) {
  1240. t.Fatalf("body = %q, want %q", g, string(w))
  1241. }
  1242. }
  1243. func TestServeVersionFails(t *testing.T) {
  1244. for _, m := range []string{
  1245. "CONNECT", "TRACE", "PUT", "POST", "HEAD",
  1246. } {
  1247. req, err := http.NewRequest(m, "", nil)
  1248. if err != nil {
  1249. t.Fatalf("error creating request: %v", err)
  1250. }
  1251. rw := httptest.NewRecorder()
  1252. serveVersion(rw, req)
  1253. if rw.Code != http.StatusMethodNotAllowed {
  1254. t.Errorf("method %s: code=%d, want %d", m, rw.Code, http.StatusMethodNotAllowed)
  1255. }
  1256. }
  1257. }
  1258. func TestBadServeKeys(t *testing.T) {
  1259. testBadCases := []struct {
  1260. req *http.Request
  1261. server etcdserver.Server
  1262. wcode int
  1263. wbody string
  1264. }{
  1265. {
  1266. // bad method
  1267. &http.Request{
  1268. Method: "CONNECT",
  1269. },
  1270. &resServer{},
  1271. http.StatusMethodNotAllowed,
  1272. "Method Not Allowed",
  1273. },
  1274. {
  1275. // bad method
  1276. &http.Request{
  1277. Method: "TRACE",
  1278. },
  1279. &resServer{},
  1280. http.StatusMethodNotAllowed,
  1281. "Method Not Allowed",
  1282. },
  1283. {
  1284. // parseRequest error
  1285. &http.Request{
  1286. Body: nil,
  1287. Method: "PUT",
  1288. },
  1289. &resServer{},
  1290. http.StatusBadRequest,
  1291. `{"errorCode":210,"message":"Invalid POST form","cause":"missing form body","index":0}`,
  1292. },
  1293. {
  1294. // etcdserver.Server error
  1295. mustNewRequest(t, "foo"),
  1296. &errServer{
  1297. errors.New("Internal Server Error"),
  1298. },
  1299. http.StatusInternalServerError,
  1300. `{"message":"Internal Server Error"}`,
  1301. },
  1302. {
  1303. // etcdserver.Server etcd error
  1304. mustNewRequest(t, "foo"),
  1305. &errServer{
  1306. etcdErr.NewError(etcdErr.EcodeKeyNotFound, "/1/pant", 0),
  1307. },
  1308. http.StatusNotFound,
  1309. `{"errorCode":100,"message":"Key not found","cause":"/pant","index":0}`,
  1310. },
  1311. {
  1312. // non-event/watcher response from etcdserver.Server
  1313. mustNewRequest(t, "foo"),
  1314. &resServer{
  1315. etcdserver.Response{},
  1316. },
  1317. http.StatusInternalServerError,
  1318. `{"message":"Internal Server Error"}`,
  1319. },
  1320. }
  1321. for i, tt := range testBadCases {
  1322. h := &keysHandler{
  1323. timeout: 0, // context times out immediately
  1324. server: tt.server,
  1325. clusterInfo: &fakeCluster{id: 1},
  1326. }
  1327. rw := httptest.NewRecorder()
  1328. h.ServeHTTP(rw, tt.req)
  1329. if rw.Code != tt.wcode {
  1330. t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode)
  1331. }
  1332. if rw.Code != http.StatusMethodNotAllowed {
  1333. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  1334. wcid := h.clusterInfo.ID().String()
  1335. if gcid != wcid {
  1336. t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
  1337. }
  1338. }
  1339. if g := strings.TrimSuffix(rw.Body.String(), "\n"); g != tt.wbody {
  1340. t.Errorf("#%d: body = %s, want %s", i, g, tt.wbody)
  1341. }
  1342. }
  1343. }
  1344. func TestServeKeysGood(t *testing.T) {
  1345. tests := []struct {
  1346. req *http.Request
  1347. wcode int
  1348. }{
  1349. {
  1350. mustNewMethodRequest(t, "HEAD", "foo"),
  1351. http.StatusOK,
  1352. },
  1353. {
  1354. mustNewMethodRequest(t, "GET", "foo"),
  1355. http.StatusOK,
  1356. },
  1357. {
  1358. mustNewForm(t, "foo", url.Values{"value": []string{"bar"}}),
  1359. http.StatusOK,
  1360. },
  1361. {
  1362. mustNewMethodRequest(t, "DELETE", "foo"),
  1363. http.StatusOK,
  1364. },
  1365. {
  1366. mustNewPostForm(t, "foo", url.Values{"value": []string{"bar"}}),
  1367. http.StatusOK,
  1368. },
  1369. }
  1370. server := &resServer{
  1371. etcdserver.Response{
  1372. Event: &store.Event{
  1373. Action: store.Get,
  1374. Node: &store.NodeExtern{},
  1375. },
  1376. },
  1377. }
  1378. for i, tt := range tests {
  1379. h := &keysHandler{
  1380. timeout: time.Hour,
  1381. server: server,
  1382. timer: &dummyRaftTimer{},
  1383. clusterInfo: &fakeCluster{id: 1},
  1384. }
  1385. rw := httptest.NewRecorder()
  1386. h.ServeHTTP(rw, tt.req)
  1387. if rw.Code != tt.wcode {
  1388. t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode)
  1389. }
  1390. }
  1391. }
  1392. func TestServeKeysEvent(t *testing.T) {
  1393. req := mustNewRequest(t, "foo")
  1394. server := &resServer{
  1395. etcdserver.Response{
  1396. Event: &store.Event{
  1397. Action: store.Get,
  1398. Node: &store.NodeExtern{},
  1399. },
  1400. },
  1401. }
  1402. h := &keysHandler{
  1403. timeout: time.Hour,
  1404. server: server,
  1405. clusterInfo: &fakeCluster{id: 1},
  1406. timer: &dummyRaftTimer{},
  1407. }
  1408. rw := httptest.NewRecorder()
  1409. h.ServeHTTP(rw, req)
  1410. wcode := http.StatusOK
  1411. wbody := mustMarshalEvent(
  1412. t,
  1413. &store.Event{
  1414. Action: store.Get,
  1415. Node: &store.NodeExtern{},
  1416. },
  1417. )
  1418. if rw.Code != wcode {
  1419. t.Errorf("got code=%d, want %d", rw.Code, wcode)
  1420. }
  1421. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  1422. wcid := h.clusterInfo.ID().String()
  1423. if gcid != wcid {
  1424. t.Errorf("cid = %s, want %s", gcid, wcid)
  1425. }
  1426. g := rw.Body.String()
  1427. if g != wbody {
  1428. t.Errorf("got body=%#v, want %#v", g, wbody)
  1429. }
  1430. }
  1431. func TestServeKeysWatch(t *testing.T) {
  1432. req := mustNewRequest(t, "/foo/bar")
  1433. ec := make(chan *store.Event)
  1434. dw := &dummyWatcher{
  1435. echan: ec,
  1436. }
  1437. server := &resServer{
  1438. etcdserver.Response{
  1439. Watcher: dw,
  1440. },
  1441. }
  1442. h := &keysHandler{
  1443. timeout: time.Hour,
  1444. server: server,
  1445. clusterInfo: &fakeCluster{id: 1},
  1446. timer: &dummyRaftTimer{},
  1447. }
  1448. go func() {
  1449. ec <- &store.Event{
  1450. Action: store.Get,
  1451. Node: &store.NodeExtern{},
  1452. }
  1453. }()
  1454. rw := httptest.NewRecorder()
  1455. h.ServeHTTP(rw, req)
  1456. wcode := http.StatusOK
  1457. wbody := mustMarshalEvent(
  1458. t,
  1459. &store.Event{
  1460. Action: store.Get,
  1461. Node: &store.NodeExtern{},
  1462. },
  1463. )
  1464. if rw.Code != wcode {
  1465. t.Errorf("got code=%d, want %d", rw.Code, wcode)
  1466. }
  1467. gcid := rw.Header().Get("X-Etcd-Cluster-ID")
  1468. wcid := h.clusterInfo.ID().String()
  1469. if gcid != wcid {
  1470. t.Errorf("cid = %s, want %s", gcid, wcid)
  1471. }
  1472. g := rw.Body.String()
  1473. if g != wbody {
  1474. t.Errorf("got body=%#v, want %#v", g, wbody)
  1475. }
  1476. }
  1477. type recordingCloseNotifier struct {
  1478. *httptest.ResponseRecorder
  1479. cn chan bool
  1480. }
  1481. func (rcn *recordingCloseNotifier) CloseNotify() <-chan bool {
  1482. return rcn.cn
  1483. }
  1484. func TestHandleWatch(t *testing.T) {
  1485. defaultRwRr := func() (http.ResponseWriter, *httptest.ResponseRecorder) {
  1486. r := httptest.NewRecorder()
  1487. return r, r
  1488. }
  1489. noopEv := func(chan *store.Event) {}
  1490. tests := []struct {
  1491. getCtx func() context.Context
  1492. getRwRr func() (http.ResponseWriter, *httptest.ResponseRecorder)
  1493. doToChan func(chan *store.Event)
  1494. wbody string
  1495. }{
  1496. {
  1497. // Normal case: one event
  1498. context.Background,
  1499. defaultRwRr,
  1500. func(ch chan *store.Event) {
  1501. ch <- &store.Event{
  1502. Action: store.Get,
  1503. Node: &store.NodeExtern{},
  1504. }
  1505. },
  1506. mustMarshalEvent(
  1507. t,
  1508. &store.Event{
  1509. Action: store.Get,
  1510. Node: &store.NodeExtern{},
  1511. },
  1512. ),
  1513. },
  1514. {
  1515. // Channel is closed, no event
  1516. context.Background,
  1517. defaultRwRr,
  1518. func(ch chan *store.Event) {
  1519. close(ch)
  1520. },
  1521. "",
  1522. },
  1523. {
  1524. // Simulate a timed-out context
  1525. func() context.Context {
  1526. ctx, cancel := context.WithCancel(context.Background())
  1527. cancel()
  1528. return ctx
  1529. },
  1530. defaultRwRr,
  1531. noopEv,
  1532. "",
  1533. },
  1534. {
  1535. // Close-notifying request
  1536. context.Background,
  1537. func() (http.ResponseWriter, *httptest.ResponseRecorder) {
  1538. rw := &recordingCloseNotifier{
  1539. ResponseRecorder: httptest.NewRecorder(),
  1540. cn: make(chan bool, 1),
  1541. }
  1542. rw.cn <- true
  1543. return rw, rw.ResponseRecorder
  1544. },
  1545. noopEv,
  1546. "",
  1547. },
  1548. }
  1549. for i, tt := range tests {
  1550. rw, rr := tt.getRwRr()
  1551. wa := &dummyWatcher{
  1552. echan: make(chan *store.Event, 1),
  1553. sidx: 10,
  1554. }
  1555. tt.doToChan(wa.echan)
  1556. handleKeyWatch(tt.getCtx(), rw, wa, false, dummyRaftTimer{})
  1557. wcode := http.StatusOK
  1558. wct := "application/json"
  1559. wei := "10"
  1560. wri := "100"
  1561. wrt := "5"
  1562. if rr.Code != wcode {
  1563. t.Errorf("#%d: got code=%d, want %d", i, rr.Code, wcode)
  1564. }
  1565. h := rr.Header()
  1566. if ct := h.Get("Content-Type"); ct != wct {
  1567. t.Errorf("#%d: Content-Type=%q, want %q", i, ct, wct)
  1568. }
  1569. if ei := h.Get("X-Etcd-Index"); ei != wei {
  1570. t.Errorf("#%d: X-Etcd-Index=%q, want %q", i, ei, wei)
  1571. }
  1572. if ri := h.Get("X-Raft-Index"); ri != wri {
  1573. t.Errorf("#%d: X-Raft-Index=%q, want %q", i, ri, wri)
  1574. }
  1575. if rt := h.Get("X-Raft-Term"); rt != wrt {
  1576. t.Errorf("#%d: X-Raft-Term=%q, want %q", i, rt, wrt)
  1577. }
  1578. g := rr.Body.String()
  1579. if g != tt.wbody {
  1580. t.Errorf("#%d: got body=%#v, want %#v", i, g, tt.wbody)
  1581. }
  1582. }
  1583. }
  1584. func TestHandleWatchStreaming(t *testing.T) {
  1585. rw := &flushingRecorder{
  1586. httptest.NewRecorder(),
  1587. make(chan struct{}, 1),
  1588. }
  1589. wa := &dummyWatcher{
  1590. echan: make(chan *store.Event),
  1591. }
  1592. // Launch the streaming handler in the background with a cancellable context
  1593. ctx, cancel := context.WithCancel(context.Background())
  1594. done := make(chan struct{})
  1595. go func() {
  1596. handleKeyWatch(ctx, rw, wa, true, dummyRaftTimer{})
  1597. close(done)
  1598. }()
  1599. // Expect one Flush for the headers etc.
  1600. select {
  1601. case <-rw.ch:
  1602. case <-time.After(time.Second):
  1603. t.Fatalf("timed out waiting for flush")
  1604. }
  1605. // Expect headers but no body
  1606. wcode := http.StatusOK
  1607. wct := "application/json"
  1608. wbody := ""
  1609. if rw.Code != wcode {
  1610. t.Errorf("got code=%d, want %d", rw.Code, wcode)
  1611. }
  1612. h := rw.Header()
  1613. if ct := h.Get("Content-Type"); ct != wct {
  1614. t.Errorf("Content-Type=%q, want %q", ct, wct)
  1615. }
  1616. g := rw.Body.String()
  1617. if g != wbody {
  1618. t.Errorf("got body=%#v, want %#v", g, wbody)
  1619. }
  1620. // Now send the first event
  1621. select {
  1622. case wa.echan <- &store.Event{
  1623. Action: store.Get,
  1624. Node: &store.NodeExtern{},
  1625. }:
  1626. case <-time.After(time.Second):
  1627. t.Fatal("timed out waiting for send")
  1628. }
  1629. // Wait for it to be flushed...
  1630. select {
  1631. case <-rw.ch:
  1632. case <-time.After(time.Second):
  1633. t.Fatalf("timed out waiting for flush")
  1634. }
  1635. // And check the body is as expected
  1636. wbody = mustMarshalEvent(
  1637. t,
  1638. &store.Event{
  1639. Action: store.Get,
  1640. Node: &store.NodeExtern{},
  1641. },
  1642. )
  1643. g = rw.Body.String()
  1644. if g != wbody {
  1645. t.Errorf("got body=%#v, want %#v", g, wbody)
  1646. }
  1647. // Rinse and repeat
  1648. select {
  1649. case wa.echan <- &store.Event{
  1650. Action: store.Get,
  1651. Node: &store.NodeExtern{},
  1652. }:
  1653. case <-time.After(time.Second):
  1654. t.Fatal("timed out waiting for send")
  1655. }
  1656. select {
  1657. case <-rw.ch:
  1658. case <-time.After(time.Second):
  1659. t.Fatalf("timed out waiting for flush")
  1660. }
  1661. // This time, we expect to see both events
  1662. wbody = wbody + wbody
  1663. g = rw.Body.String()
  1664. if g != wbody {
  1665. t.Errorf("got body=%#v, want %#v", g, wbody)
  1666. }
  1667. // Finally, time out the connection and ensure the serving goroutine returns
  1668. cancel()
  1669. select {
  1670. case <-done:
  1671. case <-time.After(time.Second):
  1672. t.Fatalf("timed out waiting for done")
  1673. }
  1674. }
  1675. func TestTrimEventPrefix(t *testing.T) {
  1676. pre := "/abc"
  1677. tests := []struct {
  1678. ev *store.Event
  1679. wev *store.Event
  1680. }{
  1681. {
  1682. nil,
  1683. nil,
  1684. },
  1685. {
  1686. &store.Event{},
  1687. &store.Event{},
  1688. },
  1689. {
  1690. &store.Event{Node: &store.NodeExtern{Key: "/abc/def"}},
  1691. &store.Event{Node: &store.NodeExtern{Key: "/def"}},
  1692. },
  1693. {
  1694. &store.Event{PrevNode: &store.NodeExtern{Key: "/abc/ghi"}},
  1695. &store.Event{PrevNode: &store.NodeExtern{Key: "/ghi"}},
  1696. },
  1697. {
  1698. &store.Event{
  1699. Node: &store.NodeExtern{Key: "/abc/def"},
  1700. PrevNode: &store.NodeExtern{Key: "/abc/ghi"},
  1701. },
  1702. &store.Event{
  1703. Node: &store.NodeExtern{Key: "/def"},
  1704. PrevNode: &store.NodeExtern{Key: "/ghi"},
  1705. },
  1706. },
  1707. }
  1708. for i, tt := range tests {
  1709. ev := trimEventPrefix(tt.ev, pre)
  1710. if !reflect.DeepEqual(ev, tt.wev) {
  1711. t.Errorf("#%d: event = %+v, want %+v", i, ev, tt.wev)
  1712. }
  1713. }
  1714. }
  1715. func TestTrimNodeExternPrefix(t *testing.T) {
  1716. pre := "/abc"
  1717. tests := []struct {
  1718. n *store.NodeExtern
  1719. wn *store.NodeExtern
  1720. }{
  1721. {
  1722. nil,
  1723. nil,
  1724. },
  1725. {
  1726. &store.NodeExtern{Key: "/abc/def"},
  1727. &store.NodeExtern{Key: "/def"},
  1728. },
  1729. {
  1730. &store.NodeExtern{
  1731. Key: "/abc/def",
  1732. Nodes: []*store.NodeExtern{
  1733. {Key: "/abc/def/1"},
  1734. {Key: "/abc/def/2"},
  1735. },
  1736. },
  1737. &store.NodeExtern{
  1738. Key: "/def",
  1739. Nodes: []*store.NodeExtern{
  1740. {Key: "/def/1"},
  1741. {Key: "/def/2"},
  1742. },
  1743. },
  1744. },
  1745. }
  1746. for i, tt := range tests {
  1747. n := trimNodeExternPrefix(tt.n, pre)
  1748. if !reflect.DeepEqual(n, tt.wn) {
  1749. t.Errorf("#%d: node = %+v, want %+v", i, n, tt.wn)
  1750. }
  1751. }
  1752. }
  1753. func TestTrimPrefix(t *testing.T) {
  1754. tests := []struct {
  1755. in string
  1756. prefix string
  1757. w string
  1758. }{
  1759. {"/v2/members", "/v2/members", ""},
  1760. {"/v2/members/", "/v2/members", ""},
  1761. {"/v2/members/foo", "/v2/members", "foo"},
  1762. }
  1763. for i, tt := range tests {
  1764. if g := trimPrefix(tt.in, tt.prefix); g != tt.w {
  1765. t.Errorf("#%d: trimPrefix = %q, want %q", i, g, tt.w)
  1766. }
  1767. }
  1768. }
  1769. func TestNewMemberCollection(t *testing.T) {
  1770. fixture := []*etcdserver.Member{
  1771. &etcdserver.Member{
  1772. ID: 12,
  1773. Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"}},
  1774. RaftAttributes: etcdserver.RaftAttributes{PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"}},
  1775. },
  1776. &etcdserver.Member{
  1777. ID: 13,
  1778. Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:9090", "http://localhost:9091"}},
  1779. RaftAttributes: etcdserver.RaftAttributes{PeerURLs: []string{"http://localhost:9092", "http://localhost:9093"}},
  1780. },
  1781. }
  1782. got := newMemberCollection(fixture)
  1783. want := httptypes.MemberCollection([]httptypes.Member{
  1784. httptypes.Member{
  1785. ID: "c",
  1786. ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"},
  1787. PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"},
  1788. },
  1789. httptypes.Member{
  1790. ID: "d",
  1791. ClientURLs: []string{"http://localhost:9090", "http://localhost:9091"},
  1792. PeerURLs: []string{"http://localhost:9092", "http://localhost:9093"},
  1793. },
  1794. })
  1795. if !reflect.DeepEqual(&want, got) {
  1796. t.Fatalf("newMemberCollection failure: want=%#v, got=%#v", &want, got)
  1797. }
  1798. }
  1799. func TestNewMember(t *testing.T) {
  1800. fixture := &etcdserver.Member{
  1801. ID: 12,
  1802. Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"}},
  1803. RaftAttributes: etcdserver.RaftAttributes{PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"}},
  1804. }
  1805. got := newMember(fixture)
  1806. want := httptypes.Member{
  1807. ID: "c",
  1808. ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"},
  1809. PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"},
  1810. }
  1811. if !reflect.DeepEqual(want, got) {
  1812. t.Fatalf("newMember failure: want=%#v, got=%#v", want, got)
  1813. }
  1814. }