github.com/hooklift/nomad@v0.5.7-0.20170407200202-db11e7dd7b55/command/agent/http_test.go (about) 1 package agent 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "net/http/httptest" 11 "os" 12 "strconv" 13 "testing" 14 "time" 15 16 "github.com/hashicorp/nomad/nomad/mock" 17 "github.com/hashicorp/nomad/nomad/structs" 18 "github.com/hashicorp/nomad/testutil" 19 ) 20 21 type TestServer struct { 22 T testing.TB 23 Dir string 24 Agent *Agent 25 Server *HTTPServer 26 } 27 28 func (s *TestServer) Cleanup() { 29 s.Server.Shutdown() 30 s.Agent.Shutdown() 31 os.RemoveAll(s.Dir) 32 } 33 34 // makeHTTPServerNoLogs returns a test server with full logging. 35 func makeHTTPServer(t testing.TB, cb func(c *Config)) *TestServer { 36 return makeHTTPServerWithWriter(t, nil, cb) 37 } 38 39 // makeHTTPServerNoLogs returns a test server which only prints agent logs and 40 // no http server logs 41 func makeHTTPServerNoLogs(t testing.TB, cb func(c *Config)) *TestServer { 42 return makeHTTPServerWithWriter(t, ioutil.Discard, cb) 43 } 44 45 // makeHTTPServerWithWriter returns a test server whose logs will be written to 46 // the passed writer. If the writer is nil, the logs are written to stderr. 47 func makeHTTPServerWithWriter(t testing.TB, w io.Writer, cb func(c *Config)) *TestServer { 48 dir, agent := makeAgent(t, cb) 49 if w == nil { 50 w = agent.logOutput 51 } 52 srv, err := NewHTTPServer(agent, agent.config, w) 53 if err != nil { 54 t.Fatalf("err: %v", err) 55 } 56 s := &TestServer{ 57 T: t, 58 Dir: dir, 59 Agent: agent, 60 Server: srv, 61 } 62 return s 63 } 64 65 func BenchmarkHTTPRequests(b *testing.B) { 66 s := makeHTTPServerNoLogs(b, func(c *Config) { 67 c.Client.Enabled = false 68 }) 69 defer s.Cleanup() 70 71 job := mock.Job() 72 var allocs []*structs.Allocation 73 count := 1000 74 for i := 0; i < count; i++ { 75 alloc := mock.Alloc() 76 alloc.Job = job 77 alloc.JobID = job.ID 78 alloc.Name = fmt.Sprintf("my-job.web[%d]", i) 79 allocs = append(allocs, alloc) 80 } 81 82 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 83 return allocs[:count], nil 84 } 85 b.ResetTimer() 86 87 b.RunParallel(func(pb *testing.PB) { 88 for pb.Next() { 89 resp := httptest.NewRecorder() 90 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 91 s.Server.wrap(handler)(resp, req) 92 } 93 }) 94 } 95 96 func TestSetIndex(t *testing.T) { 97 resp := httptest.NewRecorder() 98 setIndex(resp, 1000) 99 header := resp.Header().Get("X-Nomad-Index") 100 if header != "1000" { 101 t.Fatalf("Bad: %v", header) 102 } 103 setIndex(resp, 2000) 104 if v := resp.Header()["X-Nomad-Index"]; len(v) != 1 { 105 t.Fatalf("bad: %#v", v) 106 } 107 } 108 109 func TestSetKnownLeader(t *testing.T) { 110 resp := httptest.NewRecorder() 111 setKnownLeader(resp, true) 112 header := resp.Header().Get("X-Nomad-KnownLeader") 113 if header != "true" { 114 t.Fatalf("Bad: %v", header) 115 } 116 resp = httptest.NewRecorder() 117 setKnownLeader(resp, false) 118 header = resp.Header().Get("X-Nomad-KnownLeader") 119 if header != "false" { 120 t.Fatalf("Bad: %v", header) 121 } 122 } 123 124 func TestSetLastContact(t *testing.T) { 125 resp := httptest.NewRecorder() 126 setLastContact(resp, 123456*time.Microsecond) 127 header := resp.Header().Get("X-Nomad-LastContact") 128 if header != "123" { 129 t.Fatalf("Bad: %v", header) 130 } 131 } 132 133 func TestSetMeta(t *testing.T) { 134 meta := structs.QueryMeta{ 135 Index: 1000, 136 KnownLeader: true, 137 LastContact: 123456 * time.Microsecond, 138 } 139 resp := httptest.NewRecorder() 140 setMeta(resp, &meta) 141 header := resp.Header().Get("X-Nomad-Index") 142 if header != "1000" { 143 t.Fatalf("Bad: %v", header) 144 } 145 header = resp.Header().Get("X-Nomad-KnownLeader") 146 if header != "true" { 147 t.Fatalf("Bad: %v", header) 148 } 149 header = resp.Header().Get("X-Nomad-LastContact") 150 if header != "123" { 151 t.Fatalf("Bad: %v", header) 152 } 153 } 154 155 func TestSetHeaders(t *testing.T) { 156 s := makeHTTPServer(t, nil) 157 s.Agent.config.HTTPAPIResponseHeaders = map[string]string{"foo": "bar"} 158 defer s.Cleanup() 159 160 resp := httptest.NewRecorder() 161 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 162 return &structs.Job{Name: "foo"}, nil 163 } 164 165 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 166 s.Server.wrap(handler)(resp, req) 167 header := resp.Header().Get("foo") 168 169 if header != "bar" { 170 t.Fatalf("expected header: %v, actual: %v", "bar", header) 171 } 172 173 } 174 175 func TestContentTypeIsJSON(t *testing.T) { 176 s := makeHTTPServer(t, nil) 177 defer s.Cleanup() 178 179 resp := httptest.NewRecorder() 180 181 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 182 return &structs.Job{Name: "foo"}, nil 183 } 184 185 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 186 s.Server.wrap(handler)(resp, req) 187 188 contentType := resp.Header().Get("Content-Type") 189 190 if contentType != "application/json" { 191 t.Fatalf("Content-Type header was not 'application/json'") 192 } 193 } 194 195 func TestPrettyPrint(t *testing.T) { 196 testPrettyPrint("pretty=1", true, t) 197 } 198 199 func TestPrettyPrintOff(t *testing.T) { 200 testPrettyPrint("pretty=0", false, t) 201 } 202 203 func TestPrettyPrintBare(t *testing.T) { 204 testPrettyPrint("pretty", true, t) 205 } 206 207 func testPrettyPrint(pretty string, prettyFmt bool, t *testing.T) { 208 s := makeHTTPServer(t, nil) 209 defer s.Cleanup() 210 211 r := &structs.Job{Name: "foo"} 212 213 resp := httptest.NewRecorder() 214 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 215 return r, nil 216 } 217 218 urlStr := "/v1/job/foo?" + pretty 219 req, _ := http.NewRequest("GET", urlStr, nil) 220 s.Server.wrap(handler)(resp, req) 221 222 var expected []byte 223 if prettyFmt { 224 expected, _ = json.MarshalIndent(r, "", " ") 225 expected = append(expected, "\n"...) 226 } else { 227 expected, _ = json.Marshal(r) 228 } 229 actual, err := ioutil.ReadAll(resp.Body) 230 if err != nil { 231 t.Fatalf("err: %s", err) 232 } 233 234 if !bytes.Equal(expected, actual) { 235 t.Fatalf("bad:\nexpected:\t%q\nactual:\t\t%q", string(expected), string(actual)) 236 } 237 } 238 239 func TestParseWait(t *testing.T) { 240 resp := httptest.NewRecorder() 241 var b structs.QueryOptions 242 243 req, err := http.NewRequest("GET", 244 "/v1/catalog/nodes?wait=60s&index=1000", nil) 245 if err != nil { 246 t.Fatalf("err: %v", err) 247 } 248 249 if d := parseWait(resp, req, &b); d { 250 t.Fatalf("unexpected done") 251 } 252 253 if b.MinQueryIndex != 1000 { 254 t.Fatalf("Bad: %v", b) 255 } 256 if b.MaxQueryTime != 60*time.Second { 257 t.Fatalf("Bad: %v", b) 258 } 259 } 260 261 func TestParseWait_InvalidTime(t *testing.T) { 262 resp := httptest.NewRecorder() 263 var b structs.QueryOptions 264 265 req, err := http.NewRequest("GET", 266 "/v1/catalog/nodes?wait=60foo&index=1000", nil) 267 if err != nil { 268 t.Fatalf("err: %v", err) 269 } 270 271 if d := parseWait(resp, req, &b); !d { 272 t.Fatalf("expected done") 273 } 274 275 if resp.Code != 400 { 276 t.Fatalf("bad code: %v", resp.Code) 277 } 278 } 279 280 func TestParseWait_InvalidIndex(t *testing.T) { 281 resp := httptest.NewRecorder() 282 var b structs.QueryOptions 283 284 req, err := http.NewRequest("GET", 285 "/v1/catalog/nodes?wait=60s&index=foo", nil) 286 if err != nil { 287 t.Fatalf("err: %v", err) 288 } 289 290 if d := parseWait(resp, req, &b); !d { 291 t.Fatalf("expected done") 292 } 293 294 if resp.Code != 400 { 295 t.Fatalf("bad code: %v", resp.Code) 296 } 297 } 298 299 func TestParseConsistency(t *testing.T) { 300 var b structs.QueryOptions 301 302 req, err := http.NewRequest("GET", 303 "/v1/catalog/nodes?stale", nil) 304 if err != nil { 305 t.Fatalf("err: %v", err) 306 } 307 308 parseConsistency(req, &b) 309 if !b.AllowStale { 310 t.Fatalf("Bad: %v", b) 311 } 312 313 b = structs.QueryOptions{} 314 req, err = http.NewRequest("GET", 315 "/v1/catalog/nodes?consistent", nil) 316 if err != nil { 317 t.Fatalf("err: %v", err) 318 } 319 320 parseConsistency(req, &b) 321 if b.AllowStale { 322 t.Fatalf("Bad: %v", b) 323 } 324 } 325 326 func TestParseRegion(t *testing.T) { 327 s := makeHTTPServer(t, nil) 328 defer s.Cleanup() 329 330 req, err := http.NewRequest("GET", 331 "/v1/jobs?region=foo", nil) 332 if err != nil { 333 t.Fatalf("err: %v", err) 334 } 335 336 var region string 337 s.Server.parseRegion(req, ®ion) 338 if region != "foo" { 339 t.Fatalf("bad %s", region) 340 } 341 342 region = "" 343 req, err = http.NewRequest("GET", "/v1/jobs", nil) 344 if err != nil { 345 t.Fatalf("err: %v", err) 346 } 347 348 s.Server.parseRegion(req, ®ion) 349 if region != "global" { 350 t.Fatalf("bad %s", region) 351 } 352 } 353 354 // assertIndex tests that X-Nomad-Index is set and non-zero 355 func assertIndex(t *testing.T, resp *httptest.ResponseRecorder) { 356 header := resp.Header().Get("X-Nomad-Index") 357 if header == "" || header == "0" { 358 t.Fatalf("Bad: %v", header) 359 } 360 } 361 362 // checkIndex is like assertIndex but returns an error 363 func checkIndex(resp *httptest.ResponseRecorder) error { 364 header := resp.Header().Get("X-Nomad-Index") 365 if header == "" || header == "0" { 366 return fmt.Errorf("Bad: %v", header) 367 } 368 return nil 369 } 370 371 // getIndex parses X-Nomad-Index 372 func getIndex(t *testing.T, resp *httptest.ResponseRecorder) uint64 { 373 header := resp.Header().Get("X-Nomad-Index") 374 if header == "" { 375 t.Fatalf("Bad: %v", header) 376 } 377 val, err := strconv.Atoi(header) 378 if err != nil { 379 t.Fatalf("Bad: %v", header) 380 } 381 return uint64(val) 382 } 383 384 func httpTest(t testing.TB, cb func(c *Config), f func(srv *TestServer)) { 385 s := makeHTTPServer(t, cb) 386 defer s.Cleanup() 387 testutil.WaitForLeader(t, s.Agent.RPC) 388 f(s) 389 } 390 391 func encodeReq(obj interface{}) io.ReadCloser { 392 buf := bytes.NewBuffer(nil) 393 enc := json.NewEncoder(buf) 394 enc.Encode(obj) 395 return ioutil.NopCloser(buf) 396 }