package coderd_test import ( "context" "encoding/json" "io" "net/http" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/codersdk/healthsdk" "github.com/coder/coder/v2/testutil" ) func TestDebugHealth(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() var ( calls = atomic.Int64{} ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) sessionToken string client = coderdtest.New(t, &coderdtest.Options{ HealthcheckFunc: func(_ context.Context, apiKey string) *healthsdk.HealthcheckReport { calls.Add(1) assert.Equal(t, sessionToken, apiKey) return &healthsdk.HealthcheckReport{ Time: time.Now(), } }, HealthcheckRefresh: time.Hour, // Avoid flakes. }) _ = coderdtest.CreateFirstUser(t, client) ) defer cancel() sessionToken = client.SessionToken() for i := 0; i < 10; i++ { res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil) require.NoError(t, err) _, _ = io.ReadAll(res.Body) res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) } // The healthcheck should only have been called once. require.EqualValues(t, 1, calls.Load()) }) t.Run("Forced", func(t *testing.T) { t.Parallel() var ( calls = atomic.Int64{} ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) sessionToken string client = coderdtest.New(t, &coderdtest.Options{ HealthcheckFunc: func(_ context.Context, apiKey string) *healthsdk.HealthcheckReport { calls.Add(1) assert.Equal(t, sessionToken, apiKey) return &healthsdk.HealthcheckReport{ Time: time.Now(), } }, HealthcheckRefresh: time.Hour, // Avoid flakes. }) _ = coderdtest.CreateFirstUser(t, client) ) defer cancel() sessionToken = client.SessionToken() for i := 0; i < 10; i++ { res, err := client.Request(ctx, "GET", "/api/v2/debug/health?force=true", nil) require.NoError(t, err) _, _ = io.ReadAll(res.Body) res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) } // The healthcheck func should have been called each time. require.EqualValues(t, 10, calls.Load()) }) t.Run("Timeout", func(t *testing.T) { t.Parallel() var ( // Need to ignore errors due to ctx timeout logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) client = coderdtest.New(t, &coderdtest.Options{ Logger: &logger, HealthcheckTimeout: time.Microsecond, HealthcheckFunc: func(context.Context, string) *healthsdk.HealthcheckReport { t := time.NewTimer(time.Second) defer t.Stop() select { case <-ctx.Done(): return &healthsdk.HealthcheckReport{} case <-t.C: return &healthsdk.HealthcheckReport{} } }, }) _ = coderdtest.CreateFirstUser(t, client) ) defer cancel() res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil) require.NoError(t, err) defer res.Body.Close() _, _ = io.ReadAll(res.Body) require.Equal(t, http.StatusNotFound, res.StatusCode) }) t.Run("Refresh", func(t *testing.T) { t.Parallel() var ( calls = make(chan struct{}) callsDone = make(chan struct{}) ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) client = coderdtest.New(t, &coderdtest.Options{ HealthcheckRefresh: time.Microsecond, HealthcheckFunc: func(context.Context, string) *healthsdk.HealthcheckReport { calls <- struct{}{} return &healthsdk.HealthcheckReport{} }, }) _ = coderdtest.CreateFirstUser(t, client) ) defer cancel() go func() { defer close(callsDone) <-calls <-time.After(testutil.IntervalFast) <-calls }() res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil) require.NoError(t, err) defer res.Body.Close() _, _ = io.ReadAll(res.Body) require.Equal(t, http.StatusOK, res.StatusCode) res, err = client.Request(ctx, "GET", "/api/v2/debug/health", nil) require.NoError(t, err) defer res.Body.Close() _, _ = io.ReadAll(res.Body) require.Equal(t, http.StatusOK, res.StatusCode) select { case <-callsDone: case <-ctx.Done(): t.Fatal("timed out waiting for calls to finish") } }) t.Run("Deduplicated", func(t *testing.T) { t.Parallel() var ( ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) calls int client = coderdtest.New(t, &coderdtest.Options{ HealthcheckRefresh: time.Hour, HealthcheckTimeout: time.Hour, HealthcheckFunc: func(context.Context, string) *healthsdk.HealthcheckReport { calls++ return &healthsdk.HealthcheckReport{ Time: time.Now(), } }, }) _ = coderdtest.CreateFirstUser(t, client) ) defer cancel() res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil) require.NoError(t, err) defer res.Body.Close() _, _ = io.ReadAll(res.Body) require.Equal(t, http.StatusOK, res.StatusCode) res, err = client.Request(ctx, "GET", "/api/v2/debug/health", nil) require.NoError(t, err) defer res.Body.Close() _, _ = io.ReadAll(res.Body) require.Equal(t, http.StatusOK, res.StatusCode) require.Equal(t, 1, calls) }) t.Run("Text", func(t *testing.T) { t.Parallel() var ( ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) sessionToken string client = coderdtest.New(t, &coderdtest.Options{ HealthcheckFunc: func(_ context.Context, apiKey string) *healthsdk.HealthcheckReport { assert.Equal(t, sessionToken, apiKey) return &healthsdk.HealthcheckReport{ Time: time.Now(), Healthy: true, DERP: healthsdk.DERPHealthReport{Healthy: true}, } }, }) _ = coderdtest.CreateFirstUser(t, client) ) defer cancel() sessionToken = client.SessionToken() res, err := client.Request(ctx, "GET", "/api/v2/debug/health?format=text", nil) require.NoError(t, err) defer res.Body.Close() resB, _ := io.ReadAll(res.Body) require.Equal(t, http.StatusOK, res.StatusCode) resStr := string(resB) assert.Contains(t, resStr, "healthy: true") assert.Contains(t, resStr, "derp: true") assert.Contains(t, resStr, "access_url: false") assert.Contains(t, resStr, "websocket: false") assert.Contains(t, resStr, "database: false") }) } func TestHealthSettings(t *testing.T) { t.Parallel() t.Run("InitialState", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() // given adminClient := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, adminClient) // when settings, err := healthsdk.New(adminClient).HealthSettings(ctx) require.NoError(t, err) // then require.Equal(t, healthsdk.HealthSettings{DismissedHealthchecks: []healthsdk.HealthSection{}}, settings) }) t.Run("DismissSection", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() // given adminClient := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, adminClient) expected := healthsdk.HealthSettings{ DismissedHealthchecks: []healthsdk.HealthSection{healthsdk.HealthSectionDERP, healthsdk.HealthSectionWebsocket}, } // when: dismiss "derp" and "websocket" err := healthsdk.New(adminClient).PutHealthSettings(ctx, expected) require.NoError(t, err) // then settings, err := healthsdk.New(adminClient).HealthSettings(ctx) require.NoError(t, err) require.Equal(t, expected, settings) // then res, err := adminClient.Request(ctx, "GET", "/api/v2/debug/health", nil) require.NoError(t, err) bs, err := io.ReadAll(res.Body) require.NoError(t, err) defer res.Body.Close() var hc healthsdk.HealthcheckReport require.NoError(t, json.Unmarshal(bs, &hc)) require.True(t, hc.DERP.Dismissed) require.True(t, hc.Websocket.Dismissed) }) t.Run("UnDismissSection", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() // given adminClient := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, adminClient) initial := healthsdk.HealthSettings{ DismissedHealthchecks: []healthsdk.HealthSection{healthsdk.HealthSectionDERP, healthsdk.HealthSectionWebsocket}, } err := healthsdk.New(adminClient).PutHealthSettings(ctx, initial) require.NoError(t, err) expected := healthsdk.HealthSettings{ DismissedHealthchecks: []healthsdk.HealthSection{healthsdk.HealthSectionDERP}, } // when: undismiss "websocket" err = healthsdk.New(adminClient).PutHealthSettings(ctx, expected) require.NoError(t, err) // then settings, err := healthsdk.New(adminClient).HealthSettings(ctx) require.NoError(t, err) require.Equal(t, expected, settings) // then res, err := adminClient.Request(ctx, "GET", "/api/v2/debug/health", nil) require.NoError(t, err) bs, err := io.ReadAll(res.Body) require.NoError(t, err) defer res.Body.Close() var hc healthsdk.HealthcheckReport require.NoError(t, json.Unmarshal(bs, &hc)) require.True(t, hc.DERP.Dismissed) require.False(t, hc.Websocket.Dismissed) }) t.Run("NotModified", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() // given adminClient := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, adminClient) expected := healthsdk.HealthSettings{ DismissedHealthchecks: []healthsdk.HealthSection{healthsdk.HealthSectionDERP, healthsdk.HealthSectionWebsocket}, } err := healthsdk.New(adminClient).PutHealthSettings(ctx, expected) require.NoError(t, err) // when err = healthsdk.New(adminClient).PutHealthSettings(ctx, expected) // then require.Error(t, err) require.Contains(t, err.Error(), "health settings not modified") }) } func TestDebugWebsocket(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() }) }