github.com/grahambrereton-form3/tilt@v0.10.18/internal/hud/server/server_test.go (about) 1 package server_test 2 3 import ( 4 "bytes" 5 "context" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "path/filepath" 10 "reflect" 11 "testing" 12 "time" 13 14 "github.com/grpc-ecosystem/grpc-gateway/runtime" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 "github.com/windmilleng/wmclient/pkg/analytics" 18 19 tiltanalytics "github.com/windmilleng/tilt/internal/analytics" 20 "github.com/windmilleng/tilt/internal/cloud" 21 "github.com/windmilleng/tilt/internal/cloud/cloudurl" 22 "github.com/windmilleng/tilt/internal/hud/server" 23 "github.com/windmilleng/tilt/internal/store" 24 "github.com/windmilleng/tilt/pkg/assets" 25 "github.com/windmilleng/tilt/pkg/model" 26 proto_webview "github.com/windmilleng/tilt/pkg/webview" 27 ) 28 29 func TestHandleAnalyticsEmptyRequest(t *testing.T) { 30 f := newTestFixture(t) 31 32 var jsonStr = []byte(`[]`) 33 req, err := http.NewRequest(http.MethodPost, "/api/analytics", bytes.NewBuffer(jsonStr)) 34 if err != nil { 35 t.Fatal(err) 36 } 37 req.Header.Set("Content-Type", "application/json") 38 39 rr := httptest.NewRecorder() 40 handler := http.HandlerFunc(f.serv.HandleAnalytics) 41 42 handler.ServeHTTP(rr, req) 43 44 if status := rr.Code; status != http.StatusOK { 45 t.Errorf("handler returned wrong status code: got %v want %v", 46 status, http.StatusOK) 47 } 48 } 49 50 func TestHandleAnalyticsRecordsIncr(t *testing.T) { 51 f := newTestFixture(t) 52 53 var jsonStr = []byte(`[{"verb": "incr", "name": "foo", "tags": {}}]`) 54 req, err := http.NewRequest(http.MethodPost, "/api/analytics", bytes.NewBuffer(jsonStr)) 55 if err != nil { 56 t.Fatal(err) 57 } 58 req.Header.Set("Content-Type", "application/json") 59 60 rr := httptest.NewRecorder() 61 handler := http.HandlerFunc(f.serv.HandleAnalytics) 62 63 handler.ServeHTTP(rr, req) 64 65 if status := rr.Code; status != http.StatusOK { 66 t.Errorf("handler returned wrong status code: got %v want %v", 67 status, http.StatusOK) 68 } 69 70 f.assertIncrement("foo", 1) 71 } 72 73 func TestHandleAnalyticsNonPost(t *testing.T) { 74 f := newTestFixture(t) 75 76 req, err := http.NewRequest(http.MethodGet, "/api/analytics", nil) 77 if err != nil { 78 t.Fatal(err) 79 } 80 req.Header.Set("Content-Type", "application/json") 81 82 rr := httptest.NewRecorder() 83 handler := http.HandlerFunc(f.serv.HandleAnalytics) 84 85 handler.ServeHTTP(rr, req) 86 87 if status := rr.Code; status != http.StatusBadRequest { 88 t.Errorf("handler returned wrong status code: got %v want %v", 89 status, http.StatusBadRequest) 90 } 91 } 92 93 func TestHandleAnalyticsMalformedPayload(t *testing.T) { 94 f := newTestFixture(t) 95 96 var jsonStr = []byte(`[{"Verb": ]`) 97 req, err := http.NewRequest(http.MethodPost, "/api/analytics", bytes.NewBuffer(jsonStr)) 98 if err != nil { 99 t.Fatal(err) 100 } 101 req.Header.Set("Content-Type", "application/json") 102 103 rr := httptest.NewRecorder() 104 handler := http.HandlerFunc(f.serv.HandleAnalytics) 105 106 handler.ServeHTTP(rr, req) 107 108 if status := rr.Code; status != http.StatusBadRequest { 109 t.Errorf("handler returned wrong status code: got %v want %v", 110 status, http.StatusBadRequest) 111 } 112 } 113 114 func TestHandleAnalyticsErrorsIfNotIncr(t *testing.T) { 115 f := newTestFixture(t) 116 117 var jsonStr = []byte(`[{"verb": "count", "name": "foo", "tags": {}}]`) 118 req, err := http.NewRequest(http.MethodPost, "/api/analytics", bytes.NewBuffer(jsonStr)) 119 if err != nil { 120 t.Fatal(err) 121 } 122 req.Header.Set("Content-Type", "application/json") 123 124 rr := httptest.NewRecorder() 125 handler := http.HandlerFunc(f.serv.HandleAnalytics) 126 127 handler.ServeHTTP(rr, req) 128 129 if status := rr.Code; status != http.StatusBadRequest { 130 t.Errorf("handler returned wrong status code: got %v want %v", 131 status, http.StatusBadRequest) 132 } 133 } 134 135 func TestHandleAnalyticsOptIn(t *testing.T) { 136 f := newTestFixture(t) 137 138 err := f.ta.SetUserOpt(analytics.OptDefault) 139 if err != nil { 140 t.Fatal(err) 141 } 142 143 var jsonStr = []byte(`{"opt": "opt-in"}`) 144 req, err := http.NewRequest(http.MethodPost, "/api/analytics_opt", bytes.NewBuffer(jsonStr)) 145 if err != nil { 146 t.Fatal(err) 147 } 148 req.Header.Set("Content-Type", "application/json") 149 150 rr := httptest.NewRecorder() 151 handler := http.HandlerFunc(f.serv.HandleAnalyticsOpt) 152 153 handler.ServeHTTP(rr, req) 154 155 if status := rr.Code; status != http.StatusOK { 156 t.Errorf("handler returned wrong status code: got %v want %v", 157 status, http.StatusOK) 158 } 159 160 action := store.WaitForAction(t, reflect.TypeOf(store.AnalyticsUserOptAction{}), f.getActions) 161 assert.Equal(t, store.AnalyticsUserOptAction{Opt: analytics.OptIn}, action) 162 163 f.a.Flush(time.Millisecond) 164 165 assert.Equal(t, []analytics.CountEvent{{ 166 Name: "analytics.opt.in", 167 Tags: map[string]string{"version": "v0.0.0"}, 168 N: 1, 169 }}, f.a.Counts) 170 } 171 172 func TestHandleAnalyticsOptNonPost(t *testing.T) { 173 f := newTestFixture(t) 174 175 req, err := http.NewRequest(http.MethodGet, "/api/analytics_opt", nil) 176 if err != nil { 177 t.Fatal(err) 178 } 179 req.Header.Set("Content-Type", "application/json") 180 181 rr := httptest.NewRecorder() 182 handler := http.HandlerFunc(f.serv.HandleAnalyticsOpt) 183 184 handler.ServeHTTP(rr, req) 185 186 if status := rr.Code; status != http.StatusBadRequest { 187 t.Errorf("handler returned wrong status code: got %v want %v", 188 status, http.StatusBadRequest) 189 } 190 } 191 192 func TestHandleAnalyticsOptMalformedPayload(t *testing.T) { 193 f := newTestFixture(t) 194 195 var jsonStr = []byte(`{"opt":`) 196 req, err := http.NewRequest(http.MethodPost, "/api/analytics_opt", bytes.NewBuffer(jsonStr)) 197 if err != nil { 198 t.Fatal(err) 199 } 200 req.Header.Set("Content-Type", "application/json") 201 202 rr := httptest.NewRecorder() 203 handler := http.HandlerFunc(f.serv.HandleAnalyticsOpt) 204 205 handler.ServeHTTP(rr, req) 206 207 if status := rr.Code; status != http.StatusBadRequest { 208 t.Errorf("handler returned wrong status code: got %v want %v", 209 status, http.StatusBadRequest) 210 } 211 } 212 213 func TestHandleTriggerReturnsError(t *testing.T) { 214 f := newTestFixture(t) 215 216 var jsonStr = []byte(`{"manifest_names":["foo"]}`) 217 req, err := http.NewRequest(http.MethodPost, "/api/trigger", bytes.NewBuffer(jsonStr)) 218 if err != nil { 219 t.Fatal(err) 220 } 221 req.Header.Set("Content-Type", "application/json") 222 223 rr := httptest.NewRecorder() 224 handler := http.HandlerFunc(f.serv.HandleTrigger) 225 226 handler.ServeHTTP(rr, req) 227 228 // Expect SendToTriggerQueue to fail: make sure we reply to the HTTP request 229 // with an error when this happens 230 if status := rr.Code; status != http.StatusBadRequest { 231 t.Errorf("handler returned wrong status code: got %v want %v", 232 status, http.StatusBadRequest) 233 } 234 assert.Contains(t, rr.Body.String(), "no manifest found with name") 235 } 236 237 func TestHandleTriggerTooManyManifestNames(t *testing.T) { 238 f := newTestFixture(t) 239 240 var jsonStr = []byte(`{"manifest_names":["foo", "bar"]}`) 241 req, err := http.NewRequest(http.MethodPost, "/api/trigger", bytes.NewBuffer(jsonStr)) 242 if err != nil { 243 t.Fatal(err) 244 } 245 req.Header.Set("Content-Type", "application/json") 246 247 rr := httptest.NewRecorder() 248 handler := http.HandlerFunc(f.serv.HandleTrigger) 249 250 handler.ServeHTTP(rr, req) 251 252 if status := rr.Code; status != http.StatusBadRequest { 253 t.Errorf("handler returned wrong status code: got %v want %v", 254 status, http.StatusBadRequest) 255 } 256 assert.Contains(t, rr.Body.String(), "currently supports exactly one manifest name, got 2") 257 } 258 259 func TestHandleTriggerNonPost(t *testing.T) { 260 f := newTestFixture(t) 261 262 req, err := http.NewRequest(http.MethodGet, "/api/trigger", nil) 263 if err != nil { 264 t.Fatal(err) 265 } 266 req.Header.Set("Content-Type", "application/json") 267 268 rr := httptest.NewRecorder() 269 handler := http.HandlerFunc(f.serv.HandleTrigger) 270 271 handler.ServeHTTP(rr, req) 272 273 if status := rr.Code; status != http.StatusBadRequest { 274 t.Errorf("handler returned wrong status code: got %v want %v", 275 status, http.StatusBadRequest) 276 } 277 assert.Contains(t, rr.Body.String(), "must be POST request") 278 } 279 280 func TestHandleTriggerMalformedPayload(t *testing.T) { 281 f := newTestFixture(t) 282 283 var jsonStr = []byte(`{"manifest_names":`) 284 req, err := http.NewRequest(http.MethodPost, "/api/trigger", bytes.NewBuffer(jsonStr)) 285 if err != nil { 286 t.Fatal(err) 287 } 288 req.Header.Set("Content-Type", "application/json") 289 290 rr := httptest.NewRecorder() 291 handler := http.HandlerFunc(f.serv.HandleTrigger) 292 293 handler.ServeHTTP(rr, req) 294 295 if status := rr.Code; status != http.StatusBadRequest { 296 t.Errorf("handler returned wrong status code: got %v want %v", 297 status, http.StatusBadRequest) 298 } 299 assert.Contains(t, rr.Body.String(), "error parsing JSON") 300 } 301 302 func TestSendToTriggerQueue_manualManifest(t *testing.T) { 303 f := newTestFixture(t) 304 305 mt := store.ManifestTarget{ 306 Manifest: model.Manifest{ 307 Name: "foobar", 308 TriggerMode: model.TriggerModeManualAfterInitial, 309 }, 310 } 311 state := f.st.LockMutableStateForTesting() 312 state.UpsertManifestTarget(&mt) 313 f.st.UnlockMutableState() 314 315 err := server.SendToTriggerQueue(f.st, "foobar") 316 if err != nil { 317 t.Fatal(err) 318 } 319 320 a := store.WaitForAction(t, reflect.TypeOf(server.AppendToTriggerQueueAction{}), f.getActions) 321 action, ok := a.(server.AppendToTriggerQueueAction) 322 if !ok { 323 t.Fatalf("Action was not of type 'AppendToTriggerQueueAction': %+v", action) 324 } 325 assert.Equal(t, "foobar", action.Name.String()) 326 } 327 328 func TestSendToTriggerQueue_automaticManifest(t *testing.T) { 329 f := newTestFixture(t) 330 331 mt := store.ManifestTarget{ 332 Manifest: model.Manifest{ 333 Name: "foobar", 334 TriggerMode: model.TriggerModeAuto, 335 }, 336 } 337 state := f.st.LockMutableStateForTesting() 338 state.UpsertManifestTarget(&mt) 339 f.st.UnlockMutableState() 340 341 err := server.SendToTriggerQueue(f.st, "foobar") 342 if err != nil { 343 t.Fatal(err) 344 } 345 346 a := store.WaitForAction(t, reflect.TypeOf(server.AppendToTriggerQueueAction{}), f.getActions) 347 action, ok := a.(server.AppendToTriggerQueueAction) 348 if !ok { 349 t.Fatalf("Action was not of type 'AppendToTriggerQueueAction': %+v", action) 350 } 351 assert.Equal(t, "foobar", action.Name.String()) 352 } 353 354 func TestSendToTriggerQueue_noManifestWithName(t *testing.T) { 355 f := newTestFixture(t) 356 357 err := server.SendToTriggerQueue(f.st, "foobar") 358 359 assert.EqualError(t, err, "no manifest found with name 'foobar'") 360 store.AssertNoActionOfType(t, reflect.TypeOf(server.AppendToTriggerQueueAction{}), f.getActions) 361 } 362 363 func TestHandleNewSnapshot(t *testing.T) { 364 f := newTestFixture(t) 365 366 sp := filepath.Join("..", "webview", "testdata", "snapshot.json") 367 snap, err := ioutil.ReadFile(sp) 368 if err != nil { 369 t.Fatal(err) 370 } 371 req, err := http.NewRequest(http.MethodPost, "/api/snapshot/new", bytes.NewBuffer(snap)) 372 if err != nil { 373 t.Fatal(err) 374 } 375 376 rr := httptest.NewRecorder() 377 handler := http.HandlerFunc(f.serv.HandleNewSnapshot) 378 379 handler.ServeHTTP(rr, req) 380 381 require.Equal(t, http.StatusOK, rr.Code, 382 "handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK) 383 require.Contains(t, rr.Body.String(), "https://nonexistent.example.com/snapshot/aaaaa") 384 385 lastReq := f.snapshotHTTP.lastReq 386 if assert.NotNil(t, lastReq) { 387 var snapshot proto_webview.Snapshot 388 jspb := &runtime.JSONPb{OrigName: false, EmitDefaults: true} 389 decoder := jspb.NewDecoder(lastReq.Body) 390 decoder.Decode(&snapshot) 391 assert.Equal(t, "0.10.13", snapshot.View.RunningTiltBuild.Version) 392 assert.Equal(t, "43", snapshot.SnapshotHighlight.BeginningLogID) 393 } 394 } 395 396 type serverFixture struct { 397 t *testing.T 398 serv *server.HeadsUpServer 399 a *analytics.MemoryAnalytics 400 ta *tiltanalytics.TiltAnalytics 401 st *store.Store 402 getActions func() []store.Action 403 snapshotHTTP *fakeHTTPClient 404 } 405 406 func newTestFixture(t *testing.T) *serverFixture { 407 st, getActions := store.NewStoreForTesting() 408 go st.Loop(context.Background()) 409 a := analytics.NewMemoryAnalytics() 410 opter := tiltanalytics.NewFakeOpter(analytics.OptIn) 411 a, ta := tiltanalytics.NewMemoryTiltAnalyticsForTest(opter) 412 snapshotHTTP := &fakeHTTPClient{} 413 addr := cloudurl.Address("nonexistent.example.com") 414 uploader := cloud.NewSnapshotUploader(snapshotHTTP, addr) 415 serv, err := server.ProvideHeadsUpServer(context.Background(), st, assets.NewFakeServer(), ta, uploader) 416 if err != nil { 417 t.Fatal(err) 418 } 419 420 return &serverFixture{ 421 t: t, 422 serv: serv, 423 a: a, 424 ta: ta, 425 st: st, 426 getActions: getActions, 427 snapshotHTTP: snapshotHTTP, 428 } 429 } 430 431 type fakeHTTPClient struct { 432 lastReq *http.Request 433 } 434 435 func (f *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) { 436 f.lastReq = req 437 438 return &http.Response{ 439 StatusCode: http.StatusOK, 440 Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"ID":"aaaaa"}`))), 441 }, nil 442 } 443 444 func (f *serverFixture) assertIncrement(name string, count int) { 445 runningCount := 0 446 for _, c := range f.a.Counts { 447 if c.Name == name { 448 runningCount += c.N 449 } 450 } 451 452 assert.Equalf(f.t, count, runningCount, "Expected the total count to be %d, got %d", count, runningCount) 453 }