package coderd_test import ( "context" "fmt" "io" "net/http" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/exp/slices" "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" ) func TestDeploymentInsights(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, AgentStatsRefreshInterval: time.Millisecond * 100, MetricsCacheRefreshInterval: time.Millisecond * 100, }) user := coderdtest.CreateFirstUser(t, client) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, ProvisionPlan: echo.ProvisionComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Logger: slogtest.Make(t, nil), Client: agentClient, }) defer func() { _ = agentCloser.Close() }() resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() daus, err := client.DeploymentDAUs(context.Background(), codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err) assert.NotZero(t, res.Workspaces[0].LastUsedAt) conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{ Logger: slogtest.Make(t, nil).Named("tailnet"), }) require.NoError(t, err) defer func() { _ = conn.Close() }() sshConn, err := conn.SSHClient(ctx) require.NoError(t, err) _ = sshConn.Close() wantDAUs := &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{ { Date: time.Now().UTC().Truncate(time.Hour * 24), Amount: 1, }, }, } require.Eventuallyf(t, func() bool { daus, err = client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) return len(daus.Entries) > 0 }, testutil.WaitShort, testutil.IntervalFast, "deployment daus never loaded", ) gotDAUs, err := client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) require.Equal(t, gotDAUs, wantDAUs) template, err = client.Template(ctx, template.ID) require.NoError(t, err) res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err) } func TestUserLatencyInsights(t *testing.T) { t.Parallel() logger := slogtest.Make(t, nil) client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, AgentStatsRefreshInterval: time.Millisecond * 100, }) // Create two users, one that will appear in the report and another that // won't (due to not having/using a workspace). user := coderdtest.CreateFirstUser(t, client) _, _ = coderdtest.CreateAnotherUser(t, client, user.OrganizationID) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, ProvisionPlan: echo.ProvisionComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) // Start an agent so that we can generate stats. agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Logger: logger.Named("agent"), Client: agentClient, }) defer func() { _ = agentCloser.Close() }() resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) // Start must be at the beginning of the day, initialize it early in case // the day changes so that we get the relevant stats faster. y, m, d := time.Now().UTC().Date() today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // Connect to the agent to generate usage/latency stats. conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{ Logger: logger.Named("client"), }) require.NoError(t, err) defer conn.Close() sshConn, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshConn.Close() sess, err := sshConn.NewSession() require.NoError(t, err) defer sess.Close() r, w := io.Pipe() defer r.Close() defer w.Close() sess.Stdin = r sess.Stdout = io.Discard err = sess.Start("cat") require.NoError(t, err) var userLatencies codersdk.UserLatencyInsightsResponse require.Eventuallyf(t, func() bool { // Keep connection active. _, err := w.Write([]byte("hello world\n")) if !assert.NoError(t, err) { return false } userLatencies, err = client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ StartTime: today, EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. TemplateIDs: []uuid.UUID{template.ID}, }) if !assert.NoError(t, err) { return false } return len(userLatencies.Report.Users) > 0 && userLatencies.Report.Users[0].LatencyMS.P50 > 0 }, testutil.WaitMedium, testutil.IntervalFast, "user latency is missing") // We got our latency data, close the connection. _ = sess.Close() _ = sshConn.Close() require.Len(t, userLatencies.Report.Users, 1, "want only 1 user") require.Equal(t, userLatencies.Report.Users[0].UserID, user.UserID, "want user id to match") assert.Greater(t, userLatencies.Report.Users[0].LatencyMS.P50, float64(0), "want p50 to be greater than 0") assert.Greater(t, userLatencies.Report.Users[0].LatencyMS.P95, float64(0), "want p95 to be greater than 0") } func TestUserLatencyInsights_BadRequest(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{}) _ = coderdtest.CreateFirstUser(t, client) y, m, d := time.Now().UTC().Date() today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ StartTime: today, EndTime: today.AddDate(0, 0, -1), }) assert.Error(t, err, "want error for end time before start time") _, err = client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ StartTime: today.AddDate(0, 0, -7), EndTime: today.Add(-time.Hour), }) assert.Error(t, err, "want error for end time partial day when not today") } func TestTemplateInsights(t *testing.T) { t.Parallel() const ( firstParameterName = "first_parameter" firstParameterDisplayName = "First PARAMETER" firstParameterType = "string" firstParameterDescription = "This is first parameter" firstParameterValue = "abc" secondParameterName = "second_parameter" secondParameterDisplayName = "Second PARAMETER" secondParameterType = "number" secondParameterDescription = "This is second parameter" secondParameterValue = "123" thirdParameterName = "third_parameter" thirdParameterDisplayName = "Third PARAMETER" thirdParameterType = "string" thirdParameterDescription = "This is third parameter" thirdParameterValue = "bbb" thirdParameterOptionName1 = "This is AAA" thirdParameterOptionValue1 = "aaa" thirdParameterOptionName2 = "This is BBB" thirdParameterOptionValue2 = "bbb" thirdParameterOptionName3 = "This is CCC" thirdParameterOptionValue3 = "ccc" testAppSlug = "test-app" testAppName = "Test App" testAppIcon = "/icon.png" testAppURL = "http://127.1.0.1:65536" // Not used. ) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) opts := &coderdtest.Options{ IncludeProvisionerDaemon: true, AgentStatsRefreshInterval: time.Millisecond * 100, } client, _, coderdAPI := coderdtest.NewWithAPI(t, opts) user := coderdtest.CreateFirstUser(t, client) _, otherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, ProvisionPlan: []*proto.Provision_Response{ { Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ Parameters: []*proto.RichParameter{ {Name: firstParameterName, DisplayName: firstParameterDisplayName, Type: firstParameterType, Description: firstParameterDescription, Required: true}, {Name: secondParameterName, DisplayName: secondParameterDisplayName, Type: secondParameterType, Description: secondParameterDescription, Required: true}, {Name: thirdParameterName, DisplayName: thirdParameterDisplayName, Type: thirdParameterType, Description: thirdParameterDescription, Required: true, Options: []*proto.RichParameterOption{ {Name: thirdParameterOptionName1, Value: thirdParameterOptionValue1}, {Name: thirdParameterOptionName2, Value: thirdParameterOptionValue2}, {Name: thirdParameterOptionName3, Value: thirdParameterOptionValue3}, }}, }, }, }, }, }, ProvisionApply: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ Id: uuid.NewString(), Name: "dev", Auth: &proto.Agent_Token{ Token: authToken, }, Apps: []*proto.App{ { Slug: testAppSlug, DisplayName: testAppName, Icon: testAppIcon, SharingLevel: proto.AppSharingLevel_OWNER, Url: testAppURL, }, }, }}, }}, }, }, }}, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) buildParameters := []codersdk.WorkspaceBuildParameter{ {Name: firstParameterName, Value: firstParameterValue}, {Name: secondParameterName, Value: secondParameterValue}, {Name: thirdParameterName, Value: thirdParameterValue}, } workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { cwr.RichParameterValues = buildParameters }) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) // Start an agent so that we can generate stats. agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) agentCloser := agent.New(agent.Options{ Logger: logger.Named("agent"), Client: agentClient, }) defer func() { _ = agentCloser.Close() }() resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) // Start must be at the beginning of the day, initialize it early in case // the day changes so that we get the relevant stats faster. y, m, d := time.Now().UTC().Date() today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) requestStartTime := today requestEndTime := time.Now().UTC().Truncate(time.Hour).Add(time.Hour) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // TODO(mafredri): We should prefer to set up an app and generate // data by accessing it. // Insert entries within and outside timeframe. reporter := workspaceapps.NewStatsDBReporter(coderdAPI.Database, workspaceapps.DefaultStatsDBReporterBatchSize) //nolint:gocritic // This is a test. err := reporter.Report(dbauthz.AsSystemRestricted(ctx), []workspaceapps.StatsReport{ { UserID: user.UserID, WorkspaceID: workspace.ID, AgentID: resources[0].Agents[0].ID, AccessMethod: workspaceapps.AccessMethodPath, SlugOrPort: testAppSlug, SessionID: uuid.New(), // Outside report range. SessionStartedAt: requestStartTime.Add(-1 * time.Minute), SessionEndedAt: requestStartTime, Requests: 1, }, { UserID: user.UserID, WorkspaceID: workspace.ID, AgentID: resources[0].Agents[0].ID, AccessMethod: workspaceapps.AccessMethodPath, SlugOrPort: testAppSlug, SessionID: uuid.New(), // One minute of usage (rounded up to 5 due to query intervals). // TODO(mafredri): We'll fix this in a future refactor so that it's // 1 minute increments instead of 5. SessionStartedAt: requestStartTime, SessionEndedAt: requestStartTime.Add(1 * time.Minute), Requests: 1, }, { // Other use is using users workspace, this will result in an // additional active user and more time spent in app. UserID: otherUser.ID, WorkspaceID: workspace.ID, AgentID: resources[0].Agents[0].ID, AccessMethod: workspaceapps.AccessMethodPath, SlugOrPort: testAppSlug, SessionID: uuid.New(), // One minute of usage (rounded up to 5 due to query intervals). SessionStartedAt: requestStartTime, SessionEndedAt: requestStartTime.Add(1 * time.Minute), Requests: 1, }, { UserID: user.UserID, WorkspaceID: workspace.ID, AgentID: resources[0].Agents[0].ID, AccessMethod: workspaceapps.AccessMethodPath, SlugOrPort: testAppSlug, SessionID: uuid.New(), // Five additional minutes of usage. SessionStartedAt: requestStartTime.Add(10 * time.Minute), SessionEndedAt: requestStartTime.Add(15 * time.Minute), Requests: 1, }, { UserID: user.UserID, WorkspaceID: workspace.ID, AgentID: resources[0].Agents[0].ID, AccessMethod: workspaceapps.AccessMethodPath, SlugOrPort: testAppSlug, SessionID: uuid.New(), // Outside report range. SessionStartedAt: requestEndTime, SessionEndedAt: requestEndTime.Add(1 * time.Minute), Requests: 1, }, }) require.NoError(t, err, "want no error inserting stats") // Connect to the agent to generate usage/latency stats. conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{ Logger: logger.Named("client"), }) require.NoError(t, err) defer conn.Close() sshConn, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshConn.Close() // Start an SSH session to generate SSH usage stats. sess, err := sshConn.NewSession() require.NoError(t, err) defer sess.Close() r, w := io.Pipe() defer r.Close() defer w.Close() sess.Stdin = r err = sess.Start("cat") require.NoError(t, err) // Start an rpty session to generate rpty usage stats. rpty, err := client.WorkspaceAgentReconnectingPTY(ctx, codersdk.WorkspaceAgentReconnectingPTYOpts{ AgentID: resources[0].Agents[0].ID, Reconnect: uuid.New(), Width: 80, Height: 24, }) require.NoError(t, err) defer rpty.Close() var resp codersdk.TemplateInsightsResponse var req codersdk.TemplateInsightsRequest waitForAppSeconds := func(slug string) func() bool { return func() bool { req = codersdk.TemplateInsightsRequest{ StartTime: requestStartTime, EndTime: requestEndTime, Interval: codersdk.InsightsReportIntervalDay, } resp, err = client.TemplateInsights(ctx, req) if !assert.NoError(t, err) { return false } if slices.IndexFunc(resp.Report.AppsUsage, func(au codersdk.TemplateAppUsage) bool { return au.Slug == slug && au.Seconds > 0 }) != -1 { return true } return false } } require.Eventually(t, waitForAppSeconds("reconnecting-pty"), testutil.WaitMedium, testutil.IntervalFast, "reconnecting-pty seconds missing") require.Eventually(t, waitForAppSeconds("ssh"), testutil.WaitMedium, testutil.IntervalFast, "ssh seconds missing") // We got our data, close down sessions and connections. _ = rpty.Close() _ = sess.Close() _ = sshConn.Close() assert.WithinDuration(t, req.StartTime, resp.Report.StartTime, 0) assert.WithinDuration(t, req.EndTime, resp.Report.EndTime, 0) assert.Equal(t, int64(2), resp.Report.ActiveUsers, "want two active users") var gotApps []codersdk.TemplateAppUsage // Check builtin apps usage. for _, app := range resp.Report.AppsUsage { if app.Type != codersdk.TemplateAppsTypeBuiltin { gotApps = append(gotApps, app) continue } if slices.Contains([]string{"reconnecting-pty", "ssh"}, app.Slug) { assert.Equal(t, app.Seconds, int64(300), "want app %q to have 5 minutes of usage", app.Slug) } else { assert.Equal(t, app.Seconds, int64(0), "want app %q to have 0 minutes of usage", app.Slug) } } // Check app usage. assert.Len(t, gotApps, 1, "want one app") assert.Equal(t, []codersdk.TemplateAppUsage{ { TemplateIDs: []uuid.UUID{template.ID}, Type: codersdk.TemplateAppsTypeApp, Slug: testAppSlug, DisplayName: testAppName, Icon: testAppIcon, Seconds: 300 + 300 + 300, // Three times 5 minutes of usage (actually 1 + 1 + 5, but see TODO above). }, }, gotApps, "want app usage to match") // The full timeframe is <= 24h, so the interval matches exactly. require.Len(t, resp.IntervalReports, 1, "want one interval report") assert.WithinDuration(t, req.StartTime, resp.IntervalReports[0].StartTime, 0) assert.WithinDuration(t, req.EndTime, resp.IntervalReports[0].EndTime, 0) assert.Equal(t, int64(2), resp.IntervalReports[0].ActiveUsers, "want two active users in the interval report") // The workspace uses 3 parameters require.Len(t, resp.Report.ParametersUsage, 3) assert.Equal(t, firstParameterName, resp.Report.ParametersUsage[0].Name) assert.Equal(t, firstParameterType, resp.Report.ParametersUsage[0].Type) assert.Equal(t, firstParameterDescription, resp.Report.ParametersUsage[0].Description) assert.Equal(t, firstParameterDisplayName, resp.Report.ParametersUsage[0].DisplayName) assert.Contains(t, resp.Report.ParametersUsage[0].Values, codersdk.TemplateParameterValue{ Value: firstParameterValue, Count: 1, }) assert.Contains(t, resp.Report.ParametersUsage[0].TemplateIDs, template.ID) assert.Empty(t, resp.Report.ParametersUsage[0].Options) assert.Equal(t, secondParameterName, resp.Report.ParametersUsage[1].Name) assert.Equal(t, secondParameterType, resp.Report.ParametersUsage[1].Type) assert.Equal(t, secondParameterDescription, resp.Report.ParametersUsage[1].Description) assert.Equal(t, secondParameterDisplayName, resp.Report.ParametersUsage[1].DisplayName) assert.Contains(t, resp.Report.ParametersUsage[1].Values, codersdk.TemplateParameterValue{ Value: secondParameterValue, Count: 1, }) assert.Contains(t, resp.Report.ParametersUsage[1].TemplateIDs, template.ID) assert.Empty(t, resp.Report.ParametersUsage[1].Options) assert.Equal(t, thirdParameterName, resp.Report.ParametersUsage[2].Name) assert.Equal(t, thirdParameterType, resp.Report.ParametersUsage[2].Type) assert.Equal(t, thirdParameterDescription, resp.Report.ParametersUsage[2].Description) assert.Equal(t, thirdParameterDisplayName, resp.Report.ParametersUsage[2].DisplayName) assert.Contains(t, resp.Report.ParametersUsage[2].Values, codersdk.TemplateParameterValue{ Value: thirdParameterValue, Count: 1, }) assert.Contains(t, resp.Report.ParametersUsage[2].TemplateIDs, template.ID) assert.Equal(t, []codersdk.TemplateVersionParameterOption{ {Name: thirdParameterOptionName1, Value: thirdParameterOptionValue1}, {Name: thirdParameterOptionName2, Value: thirdParameterOptionValue2}, {Name: thirdParameterOptionName3, Value: thirdParameterOptionValue3}, }, resp.Report.ParametersUsage[2].Options) } func TestTemplateInsights_BadRequest(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{}) _ = coderdtest.CreateFirstUser(t, client) y, m, d := time.Now().UTC().Date() today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{ StartTime: today, EndTime: today.AddDate(0, 0, -1), }) assert.Error(t, err, "want error for end time before start time") _, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{ StartTime: today.AddDate(0, 0, -7), EndTime: today.Add(-time.Hour), }) assert.Error(t, err, "want error for end time partial day when not today") _, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{ StartTime: today.AddDate(0, 0, -1), EndTime: today, Interval: "invalid", }) assert.Error(t, err, "want error for bad interval") } func TestTemplateInsights_RBAC(t *testing.T) { t.Parallel() y, m, d := time.Now().UTC().Date() today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) type test struct { interval codersdk.InsightsReportInterval withTemplate bool } tests := []test{ {codersdk.InsightsReportIntervalDay, true}, {codersdk.InsightsReportIntervalDay, false}, {"", true}, {"", false}, } for _, tt := range tests { tt := tt t.Run(fmt.Sprintf("with interval=%q", tt.interval), func(t *testing.T) { t.Parallel() t.Run("AsOwner", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{}) admin := coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() var templateIDs []uuid.UUID if tt.withTemplate { version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) templateIDs = append(templateIDs, template.ID) } _, err := client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{ StartTime: today.AddDate(0, 0, -1), EndTime: today, Interval: tt.interval, TemplateIDs: templateIDs, }) require.NoError(t, err) }) t.Run("AsTemplateAdmin", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{}) admin := coderdtest.CreateFirstUser(t, client) templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() var templateIDs []uuid.UUID if tt.withTemplate { version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) templateIDs = append(templateIDs, template.ID) } _, err := templateAdmin.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{ StartTime: today.AddDate(0, 0, -1), EndTime: today, Interval: tt.interval, TemplateIDs: templateIDs, }) require.NoError(t, err) }) t.Run("AsRegularUser", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{}) admin := coderdtest.CreateFirstUser(t, client) regular, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() var templateIDs []uuid.UUID if tt.withTemplate { version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) templateIDs = append(templateIDs, template.ID) } _, err := regular.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{ StartTime: today.AddDate(0, 0, -1), EndTime: today, Interval: tt.interval, TemplateIDs: templateIDs, }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) }) } } func TestUserLatencyInsights_RBAC(t *testing.T) { t.Parallel() y, m, d := time.Now().UTC().Date() today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) type test struct { interval codersdk.InsightsReportInterval withTemplate bool } tests := []test{ {codersdk.InsightsReportIntervalDay, true}, {codersdk.InsightsReportIntervalDay, false}, {"", true}, {"", false}, } for _, tt := range tests { tt := tt t.Run(fmt.Sprintf("with interval=%q", tt.interval), func(t *testing.T) { t.Parallel() t.Run("AsOwner", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{}) admin := coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() var templateIDs []uuid.UUID if tt.withTemplate { version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) templateIDs = append(templateIDs, template.ID) } _, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ StartTime: today, EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. TemplateIDs: templateIDs, }) require.NoError(t, err) }) t.Run("AsTemplateAdmin", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{}) admin := coderdtest.CreateFirstUser(t, client) templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() var templateIDs []uuid.UUID if tt.withTemplate { version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) templateIDs = append(templateIDs, template.ID) } _, err := templateAdmin.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ StartTime: today, EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. TemplateIDs: templateIDs, }) require.NoError(t, err) }) t.Run("AsRegularUser", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{}) admin := coderdtest.CreateFirstUser(t, client) regular, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() var templateIDs []uuid.UUID if tt.withTemplate { version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) templateIDs = append(templateIDs, template.ID) } _, err := regular.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ StartTime: today, EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. TemplateIDs: templateIDs, }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) }) } }