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  }