github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/hud/server/server_test.go (about) 1 package server_test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "net/http" 9 "net/http/httptest" 10 "reflect" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 "k8s.io/apimachinery/pkg/types" 19 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 20 21 tiltanalytics "github.com/tilt-dev/tilt/internal/analytics" 22 "github.com/tilt-dev/tilt/internal/controllers/fake" 23 "github.com/tilt-dev/tilt/internal/hud/server" 24 "github.com/tilt-dev/tilt/internal/hud/view" 25 "github.com/tilt-dev/tilt/internal/sliceutils" 26 "github.com/tilt-dev/tilt/internal/store" 27 "github.com/tilt-dev/tilt/internal/testutils" 28 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 29 "github.com/tilt-dev/tilt/pkg/assets" 30 "github.com/tilt-dev/tilt/pkg/model" 31 "github.com/tilt-dev/wmclient/pkg/analytics" 32 ) 33 34 func TestHandleAnalyticsEmptyRequest(t *testing.T) { 35 f := newTestFixture(t) 36 37 status, _ := f.makeReq("/api/analytics", f.serv.HandleAnalytics, http.MethodPost, "[]") 38 require.Equal(t, http.StatusOK, status, "handler returned wrong status code") 39 } 40 41 func TestHandleAnalyticsRecordsIncr(t *testing.T) { 42 f := newTestFixture(t) 43 44 payload := `[{"verb": "incr", "name": "foo", "tags": {}}]` 45 46 status, _ := f.makeReq("/api/analytics", f.serv.HandleAnalytics, http.MethodPost, payload) 47 require.Equal(t, http.StatusOK, status, "handler returned wrong status code") 48 49 f.assertIncrement("foo", 1) 50 } 51 52 func TestHandleAnalyticsNonPost(t *testing.T) { 53 f := newTestFixture(t) 54 55 status, respBody := f.makeReq("/api/analytics", f.serv.HandleAnalytics, http.MethodGet, "") 56 57 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 58 require.Contains(t, respBody, "must be POST request") 59 } 60 61 func TestHandleAnalyticsMalformedPayload(t *testing.T) { 62 f := newTestFixture(t) 63 64 payload := `[{"Verb": ]` 65 status, respBody := f.makeReq("/api/analytics", f.serv.HandleAnalytics, http.MethodPost, payload) 66 67 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 68 require.Contains(t, respBody, "error parsing JSON") 69 } 70 71 func TestHandleAnalyticsErrorsIfNotIncr(t *testing.T) { 72 f := newTestFixture(t) 73 74 payload := `[{"verb": "count", "name": "foo", "tags": {}}]` 75 status, respBody := f.makeReq("/api/analytics", f.serv.HandleAnalytics, http.MethodPost, payload) 76 77 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 78 require.Contains(t, respBody, "only incr verbs are supported") 79 } 80 81 func TestHandleAnalyticsOptIn(t *testing.T) { 82 f := newTestFixture(t) 83 84 err := f.ta.SetUserOpt(analytics.OptDefault) 85 if err != nil { 86 t.Fatal(err) 87 } 88 89 payload := `{"opt": "opt-in"}` 90 status, _ := f.makeReq("/api/analytics", f.serv.HandleAnalyticsOpt, http.MethodPost, payload) 91 92 require.Equal(t, http.StatusOK, status, "handler returned wrong status code") 93 94 action := store.WaitForAction(t, reflect.TypeOf(store.AnalyticsUserOptAction{}), f.getActions) 95 assert.Equal(t, store.AnalyticsUserOptAction{Opt: analytics.OptIn}, action) 96 97 f.a.Flush(time.Millisecond) 98 99 assert.Equal(t, []analytics.CountEvent{{ 100 Name: "analytics.opt.in", 101 N: 1, 102 }}, f.a.Counts) 103 } 104 105 func TestHandleAnalyticsOptNonPost(t *testing.T) { 106 f := newTestFixture(t) 107 status, respBody := f.makeReq("/api/analytics", f.serv.HandleAnalyticsOpt, http.MethodGet, "") 108 109 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 110 require.Contains(t, respBody, "must be POST request") 111 } 112 113 func TestHandleAnalyticsOptMalformedPayload(t *testing.T) { 114 f := newTestFixture(t) 115 116 payload := `{"opt":` 117 status, respBody := f.makeReq("/api/analytics", f.serv.HandleAnalyticsOpt, http.MethodPost, payload) 118 119 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 120 require.Contains(t, respBody, "error parsing JSON") 121 } 122 123 func TestHandleTriggerNoManifestWithName(t *testing.T) { 124 f := newTestFixture(t) 125 126 payload := `{"manifest_names":["foo"]}` 127 status, respBody := f.makeReq("/api/trigger", f.serv.HandleTrigger, http.MethodPost, payload) 128 129 require.Equal(t, http.StatusNotFound, status, "handler returned wrong status code") 130 require.Equal(t, respBody, "resource \"foo\" does not exist\n") 131 } 132 133 func TestHandleTriggerTooManyManifestNames(t *testing.T) { 134 f := newTestFixture(t) 135 136 payload := `{"manifest_names":["foo", "bar"]}` 137 status, respBody := f.makeReq("/api/trigger", f.serv.HandleTrigger, http.MethodPost, payload) 138 139 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 140 require.Contains(t, respBody, "currently supports exactly one manifest name, got 2") 141 } 142 143 func TestHandleTriggerNonPost(t *testing.T) { 144 f := newTestFixture(t) 145 146 status, respBody := f.makeReq("/api/trigger", f.serv.HandleTrigger, http.MethodGet, "") 147 148 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 149 require.Contains(t, respBody, "must be POST request") 150 } 151 152 func TestHandleTriggerMalformedPayload(t *testing.T) { 153 f := newTestFixture(t) 154 155 payload := `{"manifest_names":` 156 status, respBody := f.makeReq("/api/trigger", f.serv.HandleTrigger, http.MethodPost, payload) 157 158 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 159 require.Contains(t, respBody, "error parsing JSON") 160 } 161 162 func TestHandleTriggerTiltfileOK(t *testing.T) { 163 f := newTestFixture(t) 164 165 payload := fmt.Sprintf(`{"manifest_names":["%s"], "build_reason": %d}`, model.MainTiltfileManifestName, model.BuildReasonFlagTriggerWeb) 166 status, resp := f.makeReq("/api/trigger", f.serv.HandleTrigger, http.MethodPost, payload) 167 assert.Equal(t, "", resp) 168 assert.Equal(t, http.StatusOK, status) 169 170 a := store.WaitForAction(t, reflect.TypeOf(store.AppendToTriggerQueueAction{}), f.getActions) 171 action, ok := a.(store.AppendToTriggerQueueAction) 172 if !ok { 173 t.Fatalf("Action was not of type 'AppendToTriggreQueueAction': %+v", action) 174 } 175 176 expected := store.AppendToTriggerQueueAction{ 177 Name: model.MainTiltfileManifestName, 178 Reason: model.BuildReasonFlagTriggerWeb, 179 } 180 assert.Equal(t, expected, action) 181 } 182 183 func TestHandleTriggerResourceDisabled(t *testing.T) { 184 f := newTestFixture(t) 185 186 f.withDummyManifests("foo") 187 state := f.st.LockMutableStateForTesting() 188 state.ManifestTargets["foo"].State.DisableState = v1alpha1.DisableStateDisabled 189 f.st.UnlockMutableState() 190 payload := `{"manifest_names": ["foo"]}` 191 status, body := f.makeReq("/api/trigger", f.serv.HandleTrigger, http.MethodPost, payload) 192 193 require.Equal(t, http.StatusOK, status, "handler returned wrong status code") 194 require.Equal(t, "resource \"foo\" is currently disabled", body) 195 } 196 197 func TestHandleTriggerNonTiltfileManifest(t *testing.T) { 198 f := newTestFixture(t) 199 200 mt := store.ManifestTarget{ 201 Manifest: model.Manifest{ 202 Name: "foobar", 203 TriggerMode: model.TriggerModeAuto, 204 }, 205 } 206 state := f.st.LockMutableStateForTesting() 207 state.UpsertManifestTarget(&mt) 208 f.st.UnlockMutableState() 209 210 payload := fmt.Sprintf(`{"manifest_names":["%s"]}`, mt.Manifest.Name) 211 status, resp := f.makeReq("/api/trigger", f.serv.HandleTrigger, http.MethodPost, payload) 212 assert.Equal(t, "", resp) 213 assert.Equal(t, http.StatusOK, status) 214 215 a := store.WaitForAction(t, reflect.TypeOf(store.AppendToTriggerQueueAction{}), f.getActions) 216 action, ok := a.(store.AppendToTriggerQueueAction) 217 if !ok { 218 t.Fatalf("Action was not of type 'AppendToTriggerQueueAction': %+v", action) 219 } 220 assert.Equal(t, "foobar", action.Name.String()) 221 } 222 223 func TestHandleOverrideTriggerModeReturnsErrorForBadManifest(t *testing.T) { 224 f := newTestFixture(t).withDummyManifests("foo", "baz") 225 226 payload := `{"manifest_names":["foo", "bar", "baz"]}` 227 status, respBody := f.makeReq("/api/override/trigger_mode", f.serv.HandleOverrideTriggerMode, http.MethodPost, payload) 228 229 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 230 require.Contains(t, respBody, "no manifest found with name 'bar'") 231 store.AssertNoActionOfType(t, reflect.TypeOf(server.OverrideTriggerModeAction{}), f.getActions) 232 } 233 234 func TestHandleOverrideTriggerModeNonPost(t *testing.T) { 235 f := newTestFixture(t) 236 237 status, respBody := f.makeReq("/api/override/trigger_mode", f.serv.HandleOverrideTriggerMode, http.MethodGet, "") 238 239 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 240 require.Contains(t, respBody, "must be POST request") 241 store.AssertNoActionOfType(t, reflect.TypeOf(server.OverrideTriggerModeAction{}), f.getActions) 242 } 243 244 func TestHandleOverrideTriggerModeMalformedPayload(t *testing.T) { 245 f := newTestFixture(t) 246 247 payload := `{"manifest_names":` 248 status, respBody := f.makeReq("/api/override/trigger_mode", f.serv.HandleOverrideTriggerMode, http.MethodPost, payload) 249 250 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 251 require.Contains(t, respBody, "error parsing JSON") 252 store.AssertNoActionOfType(t, reflect.TypeOf(server.OverrideTriggerModeAction{}), f.getActions) 253 } 254 255 func TestHandleOverrideTriggerModeInvalidTriggerMode(t *testing.T) { 256 f := newTestFixture(t).withDummyManifests("foo") 257 258 payload := `{"manifest_names":["foo"], "trigger_mode": 12345}` 259 status, respBody := f.makeReq("/api/override/trigger_mode", f.serv.HandleOverrideTriggerMode, http.MethodPost, payload) 260 261 require.Equal(t, http.StatusBadRequest, status, "handler returned wrong status code") 262 require.Contains(t, respBody, "invalid trigger mode: 12345") 263 store.AssertNoActionOfType(t, reflect.TypeOf(server.OverrideTriggerModeAction{}), f.getActions) 264 } 265 266 func TestHandleOverrideTriggerModeDispatchesEvent(t *testing.T) { 267 f := newTestFixture(t).withDummyManifests("foo", "bar") 268 269 payload := fmt.Sprintf(`{"manifest_names":["foo", "bar"], "trigger_mode": %d}`, 270 model.TriggerModeManualWithAutoInit) 271 status, _ := f.makeReq("/api/override/trigger_mode", f.serv.HandleOverrideTriggerMode, http.MethodPost, payload) 272 273 require.Equal(t, http.StatusOK, status, "handler returned wrong status code") 274 275 a := store.WaitForAction(t, reflect.TypeOf(server.OverrideTriggerModeAction{}), f.getActions) 276 action, ok := a.(server.OverrideTriggerModeAction) 277 if !ok { 278 t.Fatalf("Action was not of type 'OverrideTriggerModeAction': %+v", action) 279 } 280 281 expected := server.OverrideTriggerModeAction{ 282 ManifestNames: []model.ManifestName{"foo", "bar"}, 283 TriggerMode: model.TriggerModeManualWithAutoInit, 284 } 285 assert.Equal(t, expected, action) 286 } 287 288 func TestSetTiltfileArgs(t *testing.T) { 289 f := newTestFixture(t) 290 291 json := `["--foo", "bar", "as df"]` 292 req, err := http.NewRequest("POST", "/api/set_tiltfile_args", strings.NewReader(json)) 293 require.NoError(t, err) 294 295 req.Header.Set("Content-Type", "application/json") 296 297 rr := httptest.NewRecorder() 298 handler := http.HandlerFunc(f.serv.HandleSetTiltfileArgs) 299 300 handler.ServeHTTP(rr, req) 301 require.Equal(t, http.StatusOK, rr.Code) 302 303 require.Eventuallyf(t, func() bool { 304 var tf v1alpha1.Tiltfile 305 err := f.ctrlClient.Get(f.ctx, types.NamespacedName{Name: view.TiltfileResourceName}, &tf) 306 if err != nil { 307 return false 308 } 309 return sliceutils.StringSliceEquals(tf.Spec.Args, []string{"--foo", "bar", "as df"}) 310 }, 311 time.Second, time.Millisecond, "args didn't show up in Tiltfile API object", 312 ) 313 } 314 315 type serverFixture struct { 316 t *testing.T 317 ctx context.Context 318 serv *server.HeadsUpServer 319 a *analytics.MemoryAnalytics 320 ta *tiltanalytics.TiltAnalytics 321 st *store.Store 322 ctrlClient ctrlclient.Client 323 getActions func() []store.Action 324 snapshotHTTP *fakeHTTPClient 325 } 326 327 func newTestFixture(t *testing.T) *serverFixture { 328 st, getActions := store.NewStoreWithFakeReducer() 329 go func() { 330 err := st.Loop(context.Background()) 331 testutils.FailOnNonCanceledErr(t, err, "store.Loop failed") 332 }() 333 opter := tiltanalytics.NewFakeOpter(analytics.OptIn) 334 a, ta := tiltanalytics.NewMemoryTiltAnalyticsForTest(opter) 335 snapshotHTTP := &fakeHTTPClient{} 336 wsl := server.NewWebsocketList() 337 ctrlClient := fake.NewFakeTiltClient() 338 _ = ctrlClient.Create(context.Background(), &v1alpha1.Tiltfile{ 339 ObjectMeta: metav1.ObjectMeta{Name: model.MainTiltfileManifestName.String()}, 340 }) 341 342 ctx := context.Background() 343 344 serv, err := server.ProvideHeadsUpServer(ctx, st, assets.NewFakeServer(), ta, wsl, ctrlClient) 345 if err != nil { 346 t.Fatal(err) 347 } 348 349 return &serverFixture{ 350 t: t, 351 ctx: ctx, 352 serv: serv, 353 a: a, 354 ta: ta, 355 st: st, 356 ctrlClient: ctrlClient, 357 getActions: getActions, 358 snapshotHTTP: snapshotHTTP, 359 } 360 } 361 362 func (f *serverFixture) makeReq(endpoint string, handler http.HandlerFunc, 363 method, body string) (statusCode int, respBody string) { 364 var reader io.Reader 365 if method == http.MethodPost { 366 reader = bytes.NewBuffer([]byte(body)) 367 } 368 req, err := http.NewRequest(method, endpoint, reader) 369 if err != nil { 370 f.t.Fatal(err) 371 } 372 req.Header.Set("Content-Type", "application/json") 373 374 rr := httptest.NewRecorder() 375 handler.ServeHTTP(rr, req) 376 377 return rr.Code, rr.Body.String() 378 } 379 380 func (f *serverFixture) withDummyManifests(mNames ...string) *serverFixture { 381 state := f.st.LockMutableStateForTesting() 382 for _, mName := range mNames { 383 m := model.Manifest{Name: model.ManifestName(mName)} 384 mt := store.NewManifestTarget(m) 385 state.UpsertManifestTarget(mt) 386 } 387 defer f.st.UnlockMutableState() 388 return f 389 } 390 391 type fakeHTTPClient struct { 392 lastReq *http.Request 393 } 394 395 func (f *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) { 396 f.lastReq = req 397 398 return &http.Response{ 399 StatusCode: http.StatusOK, 400 Body: io.NopCloser(bytes.NewReader([]byte(`{"ID":"aaaaa"}`))), 401 }, nil 402 } 403 404 func (f *serverFixture) assertIncrement(name string, count int) { 405 runningCount := 0 406 for _, c := range f.a.Counts { 407 if c.Name == name { 408 runningCount += c.N 409 } 410 } 411 412 assert.Equalf(f.t, count, runningCount, "Expected the total count to be %d, got %d", count, runningCount) 413 }