mirror of https://github.com/coder/coder.git
2349 lines
76 KiB
Go
2349 lines
76 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/slogtest"
|
|
"github.com/coder/coder/v2/agent/agenttest"
|
|
agentproto "github.com/coder/coder/v2/agent/proto"
|
|
"github.com/coder/coder/v2/coderd/batchstats"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbrollup"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"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/codersdk/workspacesdk"
|
|
"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()
|
|
|
|
clientTz, err := time.LoadLocation("America/Chicago")
|
|
require.NoError(t, err)
|
|
|
|
db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
|
|
logger := slogtest.Make(t, nil)
|
|
rollupEvents := make(chan dbrollup.Event)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: ps,
|
|
Logger: &logger,
|
|
IncludeProvisionerDaemon: true,
|
|
AgentStatsRefreshInterval: time.Millisecond * 100,
|
|
DatabaseRolluper: dbrollup.New(
|
|
logger.Named("dbrollup").Leveled(slog.LevelDebug),
|
|
db,
|
|
dbrollup.WithInterval(time.Millisecond*100),
|
|
dbrollup.WithEventChannel(rollupEvents),
|
|
),
|
|
})
|
|
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
|
|
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Pre-check, no permission issues.
|
|
daus, err := client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(clientTz))
|
|
require.NoError(t, err)
|
|
|
|
_ = agenttest.New(t, client.URL, authToken)
|
|
resources := coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait()
|
|
|
|
conn, err := workspacesdk.New(client).
|
|
DialAgent(ctx, resources[0].Agents[0].ID, &workspacesdk.DialAgentOptions{
|
|
Logger: slogtest.Make(t, nil).Named("dialagent"),
|
|
})
|
|
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)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
require.Fail(t, "timed out waiting for deployment daus to update", daus)
|
|
case <-rollupEvents:
|
|
}
|
|
|
|
daus, err = client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(clientTz))
|
|
require.NoError(t, err)
|
|
if len(daus.Entries) > 0 && daus.Entries[len(daus.Entries)-1].Amount > 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUserActivityInsights_SanityCheck(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, ps := dbtestutil.NewDB(t)
|
|
logger := slogtest.Make(t, nil)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: ps,
|
|
Logger: &logger,
|
|
IncludeProvisionerDaemon: true,
|
|
AgentStatsRefreshInterval: time.Millisecond * 100,
|
|
DatabaseRolluper: dbrollup.New(
|
|
logger.Named("dbrollup"),
|
|
db,
|
|
dbrollup.WithInterval(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.PlanComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
|
|
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Start an agent so that we can generate stats.
|
|
_ = agenttest.New(t, client.URL, authToken)
|
|
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.WaitSuperLong)
|
|
defer cancel()
|
|
|
|
// Connect to the agent to generate usage/latency stats.
|
|
conn, err := workspacesdk.New(client).
|
|
DialAgent(ctx, resources[0].Agents[0].ID, &workspacesdk.DialAgentOptions{
|
|
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 userActivities codersdk.UserActivityInsightsResponse
|
|
require.Eventuallyf(t, func() bool {
|
|
// Keep connection active.
|
|
_, err := w.Write([]byte("hello world\n"))
|
|
if !assert.NoError(t, err) {
|
|
return false
|
|
}
|
|
userActivities, err = client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{
|
|
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(userActivities.Report.Users) > 0 && userActivities.Report.Users[0].Seconds > 0
|
|
}, testutil.WaitSuperLong, testutil.IntervalMedium, "user activity is missing")
|
|
|
|
// We got our latency data, close the connection.
|
|
_ = sess.Close()
|
|
_ = sshConn.Close()
|
|
|
|
require.Len(t, userActivities.Report.Users, 1, "want only 1 user")
|
|
require.Equal(t, userActivities.Report.Users[0].UserID, user.UserID, "want user id to match")
|
|
assert.Greater(t, userActivities.Report.Users[0].Seconds, int64(0), "want usage in seconds to be greater than 0")
|
|
}
|
|
|
|
func TestUserLatencyInsights(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, ps := dbtestutil.NewDB(t)
|
|
logger := slogtest.Make(t, nil)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: ps,
|
|
Logger: &logger,
|
|
IncludeProvisionerDaemon: true,
|
|
AgentStatsRefreshInterval: time.Millisecond * 50,
|
|
DatabaseRolluper: dbrollup.New(
|
|
logger.Named("dbrollup"),
|
|
db,
|
|
dbrollup.WithInterval(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.PlanComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
|
|
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Start an agent so that we can generate stats.
|
|
_ = agenttest.New(t, client.URL, authToken)
|
|
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 := workspacesdk.New(client).
|
|
DialAgent(ctx, resources[0].Agents[0].ID, &workspacesdk.DialAgentOptions{
|
|
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")
|
|
}
|
|
|
|
func TestUserActivityInsights_BadRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
saoPaulo, err := time.LoadLocation("America/Sao_Paulo")
|
|
require.NoError(t, err)
|
|
y, m, d := time.Now().UTC().Date()
|
|
today := time.Date(y, m, d, 0, 0, 0, 0, saoPaulo)
|
|
|
|
// Prepare
|
|
client := coderdtest.New(t, &coderdtest.Options{})
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Send insights request
|
|
_, err = client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{
|
|
StartTime: today,
|
|
EndTime: today.AddDate(0, 0, -1),
|
|
})
|
|
assert.Error(t, err, "want error for end time before start time")
|
|
}
|
|
|
|
func TestTemplateInsights_Golden(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Prepare test data types.
|
|
type templateParameterOption struct {
|
|
name string
|
|
value string
|
|
}
|
|
type templateParameter struct {
|
|
name string
|
|
description string
|
|
options []templateParameterOption
|
|
}
|
|
type templateApp struct {
|
|
name string
|
|
icon string
|
|
}
|
|
type testTemplate struct {
|
|
name string
|
|
parameters []*templateParameter
|
|
apps []templateApp
|
|
|
|
// Filled later.
|
|
id uuid.UUID
|
|
}
|
|
type buildParameter struct {
|
|
templateParameter *templateParameter
|
|
value string
|
|
}
|
|
type workspaceApp templateApp
|
|
type testWorkspace struct {
|
|
name string
|
|
template *testTemplate
|
|
buildParameters []buildParameter
|
|
|
|
// Filled later.
|
|
id uuid.UUID
|
|
user any // *testUser, but it's not available yet, defined below.
|
|
agentID uuid.UUID
|
|
apps []*workspaceApp
|
|
agentClient *agentsdk.Client
|
|
}
|
|
type testUser struct {
|
|
name string
|
|
workspaces []*testWorkspace
|
|
|
|
client *codersdk.Client
|
|
sdk codersdk.User
|
|
}
|
|
|
|
// Represent agent stats, to be inserted via stats batcher.
|
|
type agentStat struct {
|
|
// Set a range via start/end, multiple stats will be generated
|
|
// within the range.
|
|
startedAt time.Time
|
|
endedAt time.Time
|
|
|
|
sessionCountVSCode int64
|
|
sessionCountJetBrains int64
|
|
sessionCountReconnectingPTY int64
|
|
sessionCountSSH int64
|
|
noConnections bool
|
|
}
|
|
// Represent app usage stats, to be inserted via stats reporter.
|
|
type appUsage struct {
|
|
app *workspaceApp
|
|
startedAt time.Time
|
|
endedAt time.Time
|
|
requests int
|
|
}
|
|
|
|
// Represent actual data being generated on a per-workspace basis.
|
|
type testDataGen struct {
|
|
agentStats []agentStat
|
|
appUsage []appUsage
|
|
}
|
|
|
|
prepareFixtureAndTestData := func(t *testing.T, makeFixture func() ([]*testTemplate, []*testUser), makeData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen) ([]*testTemplate, []*testUser, map[*testWorkspace]testDataGen) {
|
|
var stableIDs []uuid.UUID
|
|
newStableUUID := func() uuid.UUID {
|
|
stableIDs = append(stableIDs, uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-%012d", len(stableIDs)+1)))
|
|
stableID := stableIDs[len(stableIDs)-1]
|
|
return stableID
|
|
}
|
|
|
|
templates, users := makeFixture()
|
|
for _, template := range templates {
|
|
template.id = newStableUUID()
|
|
}
|
|
for _, user := range users {
|
|
for _, workspace := range user.workspaces {
|
|
workspace.user = user
|
|
for _, app := range workspace.template.apps {
|
|
app := workspaceApp(app)
|
|
workspace.apps = append(workspace.apps, &app)
|
|
}
|
|
for _, bp := range workspace.buildParameters {
|
|
foundBuildParam := false
|
|
for _, param := range workspace.template.parameters {
|
|
if bp.templateParameter == param {
|
|
foundBuildParam = true
|
|
break
|
|
}
|
|
}
|
|
require.True(t, foundBuildParam, "test bug: parameter not in workspace %s template %q", workspace.name, workspace.template.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
testData := makeData(templates, users)
|
|
// Sanity check.
|
|
for ws, data := range testData {
|
|
for _, usage := range data.appUsage {
|
|
found := false
|
|
for _, app := range ws.apps {
|
|
if usage.app == app { // Pointer equality
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
for _, user := range users {
|
|
for _, workspace := range user.workspaces {
|
|
for _, app := range workspace.apps {
|
|
if usage.app == app { // Pointer equality
|
|
require.True(t, found, "test bug: app %q not in workspace %q: want user=%s workspace=%s; got user=%s workspace=%s ", usage.app.name, ws.name, ws.user.(*testUser).name, ws.name, user.name, workspace.name)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
require.True(t, found, "test bug: app %q not in workspace %q", usage.app.name, ws.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
return templates, users, testData
|
|
}
|
|
|
|
prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) (*codersdk.Client, chan dbrollup.Event) {
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
|
|
db, ps := dbtestutil.NewDB(t)
|
|
events := make(chan dbrollup.Event)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: ps,
|
|
Logger: &logger,
|
|
IncludeProvisionerDaemon: true,
|
|
AgentStatsRefreshInterval: time.Hour, // Not relevant for this test.
|
|
DatabaseRolluper: dbrollup.New(
|
|
logger.Named("dbrollup"),
|
|
db,
|
|
dbrollup.WithInterval(time.Millisecond*50),
|
|
dbrollup.WithEventChannel(events),
|
|
),
|
|
})
|
|
|
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Prepare all test users.
|
|
for _, user := range users {
|
|
user.client, user.sdk = coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
|
|
r.Username = user.name
|
|
})
|
|
user.client.SetLogger(logger.Named("user").With(slog.Field{Name: "name", Value: user.name}))
|
|
}
|
|
|
|
// Prepare all the templates.
|
|
for _, template := range templates {
|
|
template := template
|
|
|
|
var parameters []*proto.RichParameter
|
|
for _, parameter := range template.parameters {
|
|
var options []*proto.RichParameterOption
|
|
var defaultValue string
|
|
for _, option := range parameter.options {
|
|
if defaultValue == "" {
|
|
defaultValue = option.value
|
|
}
|
|
options = append(options, &proto.RichParameterOption{
|
|
Name: option.name,
|
|
Value: option.value,
|
|
})
|
|
}
|
|
parameters = append(parameters, &proto.RichParameter{
|
|
Name: parameter.name,
|
|
DisplayName: parameter.name,
|
|
Type: "string",
|
|
Description: parameter.description,
|
|
Options: options,
|
|
DefaultValue: defaultValue,
|
|
})
|
|
}
|
|
|
|
// Prepare all workspace resources (agents and apps).
|
|
var (
|
|
createWorkspaces []func(uuid.UUID)
|
|
waitWorkspaces []func()
|
|
)
|
|
var resources []*proto.Resource
|
|
for _, user := range users {
|
|
user := user
|
|
for _, workspace := range user.workspaces {
|
|
workspace := workspace
|
|
|
|
if workspace.template != template {
|
|
continue
|
|
}
|
|
authToken := uuid.New()
|
|
agentClient := agentsdk.New(client.URL)
|
|
agentClient.SetSessionToken(authToken.String())
|
|
workspace.agentClient = agentClient
|
|
|
|
var apps []*proto.App
|
|
for _, app := range workspace.apps {
|
|
apps = append(apps, &proto.App{
|
|
Slug: app.name,
|
|
DisplayName: app.name,
|
|
Icon: app.icon,
|
|
SharingLevel: proto.AppSharingLevel_OWNER,
|
|
Url: "http://",
|
|
})
|
|
}
|
|
|
|
resources = append(resources, &proto.Resource{
|
|
Name: "example",
|
|
Type: "aws_instance",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(), // Doesn't matter, not used in DB.
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{
|
|
Token: authToken.String(),
|
|
},
|
|
Apps: apps,
|
|
}},
|
|
})
|
|
|
|
var buildParameters []codersdk.WorkspaceBuildParameter
|
|
for _, buildParameter := range workspace.buildParameters {
|
|
buildParameters = append(buildParameters, codersdk.WorkspaceBuildParameter{
|
|
Name: buildParameter.templateParameter.name,
|
|
Value: buildParameter.value,
|
|
})
|
|
}
|
|
|
|
createWorkspaces = append(createWorkspaces, func(templateID uuid.UUID) {
|
|
// Create workspace using the users client.
|
|
createdWorkspace := coderdtest.CreateWorkspace(t, user.client, firstUser.OrganizationID, templateID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.RichParameterValues = buildParameters
|
|
})
|
|
workspace.id = createdWorkspace.ID
|
|
waitWorkspaces = append(waitWorkspaces, func() {
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, user.client, createdWorkspace.LatestBuild.ID)
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
ws, err := user.client.Workspace(ctx, workspace.id)
|
|
require.NoError(t, err, "want no error getting workspace")
|
|
|
|
workspace.agentID = ws.LatestBuild.Resources[0].Agents[0].ID
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// Create the template version and template.
|
|
version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Plan{
|
|
Plan: &proto.PlanComplete{
|
|
Parameters: parameters,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{
|
|
Resources: resources,
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
// Create template, essentially a modified version of CreateTemplate
|
|
// where we can control the template ID.
|
|
// createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
|
|
createdTemplate := dbgen.Template(t, db, database.Template{
|
|
ID: template.id,
|
|
ActiveVersionID: version.ID,
|
|
OrganizationID: firstUser.OrganizationID,
|
|
CreatedBy: firstUser.UserID,
|
|
GroupACL: database.TemplateACL{
|
|
firstUser.OrganizationID.String(): []rbac.Action{rbac.ActionRead},
|
|
},
|
|
})
|
|
err := db.UpdateTemplateVersionByID(context.Background(), database.UpdateTemplateVersionByIDParams{
|
|
ID: version.ID,
|
|
TemplateID: uuid.NullUUID{
|
|
UUID: createdTemplate.ID,
|
|
Valid: true,
|
|
},
|
|
})
|
|
require.NoError(t, err, "want no error updating template version")
|
|
|
|
// Create all workspaces and wait for them.
|
|
for _, createWorkspace := range createWorkspaces {
|
|
createWorkspace(template.id)
|
|
}
|
|
for _, waitWorkspace := range waitWorkspaces {
|
|
waitWorkspace()
|
|
}
|
|
}
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
|
|
// Use agent stats batcher to insert agent stats, similar to live system.
|
|
// NOTE(mafredri): Ideally we would pass batcher as a coderd option and
|
|
// insert using the agentClient, but we have a circular dependency on
|
|
// the database.
|
|
batcher, batcherCloser, err := batchstats.New(
|
|
ctx,
|
|
batchstats.WithStore(db),
|
|
batchstats.WithLogger(logger.Named("batchstats")),
|
|
batchstats.WithInterval(time.Hour),
|
|
)
|
|
require.NoError(t, err)
|
|
defer batcherCloser() // Flushes the stats, this is to ensure they're written.
|
|
|
|
for workspace, data := range testData {
|
|
for _, stat := range data.agentStats {
|
|
createdAt := stat.startedAt
|
|
connectionCount := int64(1)
|
|
if stat.noConnections {
|
|
connectionCount = 0
|
|
}
|
|
for createdAt.Before(stat.endedAt) {
|
|
err = batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{
|
|
ConnectionCount: connectionCount,
|
|
SessionCountVscode: stat.sessionCountVSCode,
|
|
SessionCountJetbrains: stat.sessionCountJetBrains,
|
|
SessionCountReconnectingPty: stat.sessionCountReconnectingPTY,
|
|
SessionCountSsh: stat.sessionCountSSH,
|
|
})
|
|
require.NoError(t, err, "want no error inserting agent stats")
|
|
createdAt = createdAt.Add(30 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Insert app usage.
|
|
var stats []workspaceapps.StatsReport
|
|
for workspace, data := range testData {
|
|
for _, usage := range data.appUsage {
|
|
appName := usage.app.name
|
|
accessMethod := workspaceapps.AccessMethodPath
|
|
if usage.app.name == "terminal" {
|
|
appName = ""
|
|
accessMethod = workspaceapps.AccessMethodTerminal
|
|
}
|
|
stats = append(stats, workspaceapps.StatsReport{
|
|
UserID: workspace.user.(*testUser).sdk.ID,
|
|
WorkspaceID: workspace.id,
|
|
AgentID: workspace.agentID,
|
|
AccessMethod: accessMethod,
|
|
SlugOrPort: appName,
|
|
SessionID: uuid.New(),
|
|
SessionStartedAt: usage.startedAt,
|
|
SessionEndedAt: usage.endedAt,
|
|
Requests: usage.requests,
|
|
})
|
|
}
|
|
}
|
|
reporter := workspaceapps.NewStatsDBReporter(db, workspaceapps.DefaultStatsDBReporterBatchSize)
|
|
//nolint:gocritic // This is a test.
|
|
err = reporter.Report(dbauthz.AsSystemRestricted(ctx), stats)
|
|
require.NoError(t, err, "want no error inserting app stats")
|
|
|
|
return client, events
|
|
}
|
|
|
|
baseTemplateAndUserFixture := func() ([]*testTemplate, []*testUser) {
|
|
// Test templates and configuration to generate.
|
|
templates := []*testTemplate{
|
|
// Create two templates with near-identical apps and parameters
|
|
// to allow testing for grouping similar data.
|
|
{
|
|
name: "template1",
|
|
parameters: []*templateParameter{
|
|
{name: "param1", description: "This is first parameter"},
|
|
{name: "param2", description: "This is second parameter"},
|
|
{name: "param3", description: "This is third parameter"},
|
|
{
|
|
name: "param4",
|
|
description: "This is fourth parameter",
|
|
options: []templateParameterOption{
|
|
{name: "option1", value: "option1"},
|
|
{name: "option2", value: "option2"},
|
|
},
|
|
},
|
|
},
|
|
apps: []templateApp{
|
|
{name: "app1", icon: "/icon1.png"},
|
|
{name: "app2", icon: "/icon2.png"},
|
|
{name: "app3", icon: "/icon2.png"},
|
|
},
|
|
},
|
|
{
|
|
name: "template2",
|
|
parameters: []*templateParameter{
|
|
{name: "param1", description: "This is first parameter"},
|
|
{name: "param2", description: "This is second parameter"},
|
|
{name: "param3", description: "This is third parameter"},
|
|
},
|
|
apps: []templateApp{
|
|
{name: "app1", icon: "/icon1.png"},
|
|
{name: "app2", icon: "/icon2.png"},
|
|
{name: "app3", icon: "/icon2.png"},
|
|
},
|
|
},
|
|
// Create another template with different parameters and apps.
|
|
{
|
|
name: "othertemplate",
|
|
parameters: []*templateParameter{
|
|
{name: "otherparam1", description: "This is another parameter"},
|
|
},
|
|
apps: []templateApp{
|
|
{name: "otherapp1", icon: "/icon1.png"},
|
|
|
|
// This "special test app" will be converted into web
|
|
// terminal usage, this is not included in stats since we
|
|
// currently rely on agent stats for this data.
|
|
{name: "terminal", icon: "/terminal.png"},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Users and workspaces to generate.
|
|
users := []*testUser{
|
|
{
|
|
name: "user1",
|
|
workspaces: []*testWorkspace{
|
|
{
|
|
name: "workspace1",
|
|
template: templates[0],
|
|
buildParameters: []buildParameter{
|
|
{templateParameter: templates[0].parameters[0], value: "abc"},
|
|
{templateParameter: templates[0].parameters[1], value: "123"},
|
|
{templateParameter: templates[0].parameters[2], value: "bbb"},
|
|
{templateParameter: templates[0].parameters[3], value: "option1"},
|
|
},
|
|
},
|
|
{
|
|
name: "workspace2",
|
|
template: templates[1],
|
|
buildParameters: []buildParameter{
|
|
{templateParameter: templates[1].parameters[0], value: "ABC"},
|
|
{templateParameter: templates[1].parameters[1], value: "123"},
|
|
{templateParameter: templates[1].parameters[2], value: "BBB"},
|
|
},
|
|
},
|
|
{
|
|
name: "otherworkspace3",
|
|
template: templates[2],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "user2",
|
|
workspaces: []*testWorkspace{
|
|
{
|
|
name: "workspace1",
|
|
template: templates[0],
|
|
buildParameters: []buildParameter{
|
|
{templateParameter: templates[0].parameters[0], value: "abc"},
|
|
{templateParameter: templates[0].parameters[1], value: "123"},
|
|
{templateParameter: templates[0].parameters[2], value: "BBB"},
|
|
{templateParameter: templates[0].parameters[3], value: "option1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "user3",
|
|
workspaces: []*testWorkspace{
|
|
{
|
|
name: "otherworkspace1",
|
|
template: templates[2],
|
|
buildParameters: []buildParameter{
|
|
{templateParameter: templates[2].parameters[0], value: "xyz"},
|
|
},
|
|
},
|
|
{
|
|
name: "workspace2",
|
|
template: templates[0],
|
|
buildParameters: []buildParameter{
|
|
{templateParameter: templates[0].parameters[3], value: "option2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return templates, users
|
|
}
|
|
|
|
// Time range for report, test data will be generated within and
|
|
// outside this range, but only data within the range should be
|
|
// included in the report.
|
|
frozenLastNight := time.Date(2023, 8, 22, 0, 0, 0, 0, time.UTC)
|
|
frozenWeekAgo := frozenLastNight.AddDate(0, 0, -7)
|
|
|
|
saoPaulo, err := time.LoadLocation("America/Sao_Paulo")
|
|
require.NoError(t, err)
|
|
frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo)
|
|
require.NoError(t, err)
|
|
|
|
//nolint:dupl // For testing purposes
|
|
makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen {
|
|
return map[*testWorkspace]testDataGen{
|
|
users[0].workspaces[0]: {
|
|
agentStats: []agentStat{
|
|
{ // One hour of usage.
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
sessionCountVSCode: 1,
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // 12 minutes of usage.
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 1),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 1).Add(12 * time.Minute),
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // 1m30s of usage -> 2m rounded.
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(4*time.Minute + 30*time.Second),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Minute),
|
|
sessionCountJetBrains: 1,
|
|
},
|
|
},
|
|
appUsage: []appUsage{
|
|
{ // One hour of usage.
|
|
app: users[0].workspaces[0].apps[0],
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
requests: 1,
|
|
},
|
|
{ // 30s of app usage -> 1m rounded.
|
|
app: users[0].workspaces[0].apps[0],
|
|
startedAt: frozenWeekAgo.Add(2*time.Hour + 10*time.Second),
|
|
endedAt: frozenWeekAgo.Add(2*time.Hour + 40*time.Second),
|
|
requests: 1,
|
|
},
|
|
{ // 1m30s of app usage -> 2m rounded (included in São Paulo).
|
|
app: users[0].workspaces[0].apps[0],
|
|
startedAt: frozenWeekAgo.Add(3*time.Hour + 30*time.Second),
|
|
endedAt: frozenWeekAgo.Add(3*time.Hour + 90*time.Second),
|
|
requests: 1,
|
|
},
|
|
{ // used an app on the last day, counts as active user, 12m.
|
|
app: users[0].workspaces[0].apps[2],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 6),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 6).Add(12 * time.Minute),
|
|
requests: 1,
|
|
},
|
|
},
|
|
},
|
|
users[0].workspaces[1]: {
|
|
agentStats: []agentStat{
|
|
{
|
|
// One hour of usage in second template at the same time
|
|
// as in first template. When selecting both templates
|
|
// this user and their app usage will only be counted
|
|
// once but the template ID will show up in the data.
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
sessionCountVSCode: 1,
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // One hour of usage.
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, -12),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, -12).Add(time.Hour),
|
|
sessionCountSSH: 1,
|
|
sessionCountReconnectingPTY: 1,
|
|
},
|
|
{ // Another one hour of usage, but "active users" shouldn't be increased twice.
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, -10),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, -10).Add(time.Hour),
|
|
sessionCountSSH: 1,
|
|
sessionCountReconnectingPTY: 1,
|
|
},
|
|
},
|
|
appUsage: []appUsage{
|
|
{ // One hour of usage, but same user and same template app, only count once.
|
|
app: users[0].workspaces[1].apps[0],
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
requests: 1,
|
|
},
|
|
{
|
|
// Different templates but identical apps, apps will be
|
|
// combined and usage will be summed.
|
|
app: users[0].workspaces[1].apps[0],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 2),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Hour),
|
|
requests: 1,
|
|
},
|
|
},
|
|
},
|
|
users[0].workspaces[2]: {
|
|
agentStats: []agentStat{},
|
|
appUsage: []appUsage{},
|
|
},
|
|
users[1].workspaces[0]: {
|
|
agentStats: []agentStat{
|
|
{ // One hour of agent usage before timeframe (exclude).
|
|
startedAt: frozenWeekAgo.Add(-time.Hour),
|
|
endedAt: frozenWeekAgo,
|
|
sessionCountVSCode: 1,
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // One hour of usage.
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // One hour of agent usage after timeframe (exclude in UTC, include in São Paulo).
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 7),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 7).Add(time.Hour),
|
|
sessionCountVSCode: 1,
|
|
sessionCountSSH: 1,
|
|
},
|
|
},
|
|
appUsage: []appUsage{
|
|
{ // One hour of app usage before timeframe (exclude).
|
|
app: users[1].workspaces[0].apps[2],
|
|
startedAt: frozenWeekAgo.Add(-time.Hour),
|
|
endedAt: frozenWeekAgo,
|
|
requests: 1,
|
|
},
|
|
{ // One hour of app usage after timeframe (exclude in UTC, include in São Paulo).
|
|
app: users[1].workspaces[0].apps[2],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 7),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 7).Add(time.Hour),
|
|
requests: 1,
|
|
},
|
|
},
|
|
},
|
|
users[2].workspaces[0]: {
|
|
agentStats: []agentStat{
|
|
{ // One hour of usage.
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
sessionCountSSH: 1,
|
|
sessionCountReconnectingPTY: 1,
|
|
},
|
|
},
|
|
appUsage: []appUsage{
|
|
{
|
|
app: users[2].workspaces[0].apps[0],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 2),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(5 * time.Minute),
|
|
requests: 1,
|
|
},
|
|
{ // Special app; excluded from apps, but counted as active during the day.
|
|
app: users[2].workspaces[0].apps[1],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 3),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 3).Add(5 * time.Minute),
|
|
requests: 1,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
type testRequest struct {
|
|
name string
|
|
makeRequest func([]*testTemplate) codersdk.TemplateInsightsRequest
|
|
ignoreTimes bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
makeFixture func() ([]*testTemplate, []*testUser)
|
|
makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen
|
|
requests []testRequest
|
|
}{
|
|
{
|
|
name: "multiple users and workspaces",
|
|
makeFixture: baseTemplateAndUserFixture,
|
|
makeTestData: makeBaseTestData,
|
|
requests: []testRequest{
|
|
{
|
|
name: "week deployment wide",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalDay,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "weekly aggregated deployment wide",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
StartTime: frozenWeekAgo.AddDate(0, 0, -3),
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 4),
|
|
Interval: codersdk.InsightsReportIntervalWeek,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "week all templates",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalDay,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "weekly aggregated templates",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id},
|
|
StartTime: frozenWeekAgo.AddDate(0, 0, -1),
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 6),
|
|
Interval: codersdk.InsightsReportIntervalWeek,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "week first template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[0].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalDay,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "weekly aggregated first template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[0].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalWeek,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "week second template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[1].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalDay,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "three weeks second template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[1].id},
|
|
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalWeek,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "week third template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[2].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalDay,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
// São Paulo is three hours behind UTC, so we should not see
|
|
// any data between weekAgo and weekAgo.Add(3 * time.Hour).
|
|
name: "week other timezone (São Paulo)",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
StartTime: frozenWeekAgoSaoPaulo,
|
|
EndTime: frozenWeekAgoSaoPaulo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalDay,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "three weeks second template only report",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[1].id},
|
|
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalWeek,
|
|
Sections: []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionReport},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "three weeks second template only interval reports",
|
|
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
return codersdk.TemplateInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[1].id},
|
|
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
Interval: codersdk.InsightsReportIntervalWeek,
|
|
Sections: []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionIntervalReports},
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "parameters",
|
|
makeFixture: baseTemplateAndUserFixture,
|
|
makeTestData: func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen {
|
|
return map[*testWorkspace]testDataGen{}
|
|
},
|
|
requests: []testRequest{
|
|
{
|
|
// Since workspaces are created "now", we can only get
|
|
// parameters using a time range that includes "now".
|
|
// We check yesterday and today for stability just in case
|
|
// the test runs at UTC midnight.
|
|
name: "yesterday and today deployment wide",
|
|
ignoreTimes: true,
|
|
makeRequest: func(_ []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
now := time.Now().UTC()
|
|
return codersdk.TemplateInsightsRequest{
|
|
StartTime: now.Truncate(24*time.Hour).AddDate(0, 0, -1),
|
|
EndTime: now.Truncate(time.Hour).Add(time.Hour),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "two days ago, no data",
|
|
ignoreTimes: true,
|
|
makeRequest: func(_ []*testTemplate) codersdk.TemplateInsightsRequest {
|
|
twoDaysAgo := time.Now().UTC().Truncate(24*time.Hour).AddDate(0, 0, -2)
|
|
return codersdk.TemplateInsightsRequest{
|
|
StartTime: twoDaysAgo,
|
|
EndTime: twoDaysAgo.AddDate(0, 0, 1),
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.NotNil(t, tt.makeFixture, "test bug: makeFixture must be set")
|
|
require.NotNil(t, tt.makeTestData, "test bug: makeTestData must be set")
|
|
templates, users, testData := prepareFixtureAndTestData(t, tt.makeFixture, tt.makeTestData)
|
|
client, events := prepare(t, templates, users, testData)
|
|
|
|
// Drain two events, the first one resumes rolluper
|
|
// operation and the second one waits for the rollup
|
|
// to complete.
|
|
_, _ = <-events, <-events
|
|
|
|
for _, req := range tt.requests {
|
|
req := req
|
|
t.Run(req.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
report, err := client.TemplateInsights(ctx, req.makeRequest(templates))
|
|
require.NoError(t, err, "want no error getting template insights")
|
|
|
|
if req.ignoreTimes {
|
|
// Ignore times, we're only interested in the data.
|
|
report.Report.StartTime = time.Time{}
|
|
report.Report.EndTime = time.Time{}
|
|
for i := range report.IntervalReports {
|
|
report.IntervalReports[i].StartTime = time.Time{}
|
|
report.IntervalReports[i].EndTime = time.Time{}
|
|
}
|
|
}
|
|
|
|
partialName := strings.Join(strings.Split(t.Name(), "/")[1:], "_")
|
|
goldenFile := filepath.Join("testdata", "insights", "template", partialName+".json.golden")
|
|
if *updateGoldenFiles {
|
|
err = os.MkdirAll(filepath.Dir(goldenFile), 0o755)
|
|
require.NoError(t, err, "want no error creating golden file directory")
|
|
f, err := os.Create(goldenFile)
|
|
require.NoError(t, err, "want no error creating golden file")
|
|
defer f.Close()
|
|
enc := json.NewEncoder(f)
|
|
enc.SetIndent("", " ")
|
|
enc.Encode(report)
|
|
return
|
|
}
|
|
|
|
f, err := os.Open(goldenFile)
|
|
require.NoError(t, err, "open golden file, run \"make update-golden-files\" and commit the changes")
|
|
defer f.Close()
|
|
var want codersdk.TemplateInsightsResponse
|
|
err = json.NewDecoder(f).Decode(&want)
|
|
require.NoError(t, err, "want no error decoding golden file")
|
|
|
|
cmpOpts := []cmp.Option{
|
|
// Ensure readable UUIDs in diff.
|
|
cmp.Transformer("UUIDs", func(in []uuid.UUID) (s []string) {
|
|
for _, id := range in {
|
|
s = append(s, id.String())
|
|
}
|
|
return s
|
|
}),
|
|
}
|
|
// Use cmp.Diff here because it produces more readable diffs.
|
|
assert.Empty(t, cmp.Diff(want, report, cmpOpts...), "golden file mismatch (-want +got): %s, run \"make update-golden-files\", verify and commit the changes", goldenFile)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserActivityInsights_Golden(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Prepare test data types.
|
|
type templateApp struct {
|
|
name string
|
|
icon string
|
|
}
|
|
type testTemplate struct {
|
|
name string
|
|
apps []templateApp
|
|
|
|
// Filled later.
|
|
id uuid.UUID
|
|
}
|
|
type workspaceApp templateApp
|
|
type testWorkspace struct {
|
|
name string
|
|
template *testTemplate
|
|
|
|
// Filled later.
|
|
id uuid.UUID
|
|
user any // *testUser, but it's not available yet, defined below.
|
|
agentID uuid.UUID
|
|
apps []*workspaceApp
|
|
agentClient *agentsdk.Client
|
|
}
|
|
type testUser struct {
|
|
name string
|
|
workspaces []*testWorkspace
|
|
|
|
client *codersdk.Client
|
|
sdk codersdk.User
|
|
|
|
// Filled later.
|
|
id uuid.UUID
|
|
}
|
|
|
|
// Represent agent stats, to be inserted via stats batcher.
|
|
type agentStat struct {
|
|
// Set a range via start/end, multiple stats will be generated
|
|
// within the range.
|
|
startedAt time.Time
|
|
endedAt time.Time
|
|
|
|
sessionCountVSCode int64
|
|
sessionCountJetBrains int64
|
|
sessionCountReconnectingPTY int64
|
|
sessionCountSSH int64
|
|
noConnections bool
|
|
}
|
|
// Represent app usage stats, to be inserted via stats reporter.
|
|
type appUsage struct {
|
|
app *workspaceApp
|
|
startedAt time.Time
|
|
endedAt time.Time
|
|
requests int
|
|
}
|
|
|
|
// Represent actual data being generated on a per-workspace basis.
|
|
type testDataGen struct {
|
|
agentStats []agentStat
|
|
appUsage []appUsage
|
|
}
|
|
|
|
prepareFixtureAndTestData := func(t *testing.T, makeFixture func() ([]*testTemplate, []*testUser), makeData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen) ([]*testTemplate, []*testUser, map[*testWorkspace]testDataGen) {
|
|
var stableIDs []uuid.UUID
|
|
newStableUUID := func() uuid.UUID {
|
|
stableIDs = append(stableIDs, uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-%012d", len(stableIDs)+1)))
|
|
stableID := stableIDs[len(stableIDs)-1]
|
|
return stableID
|
|
}
|
|
|
|
templates, users := makeFixture()
|
|
for _, template := range templates {
|
|
template.id = newStableUUID()
|
|
}
|
|
for _, user := range users {
|
|
user.id = newStableUUID()
|
|
}
|
|
|
|
for _, user := range users {
|
|
for _, workspace := range user.workspaces {
|
|
workspace.user = user
|
|
for _, app := range workspace.template.apps {
|
|
app := workspaceApp(app)
|
|
workspace.apps = append(workspace.apps, &app)
|
|
}
|
|
}
|
|
}
|
|
|
|
testData := makeData(templates, users)
|
|
// Sanity check.
|
|
for ws, data := range testData {
|
|
for _, usage := range data.appUsage {
|
|
found := false
|
|
for _, app := range ws.apps {
|
|
if usage.app == app { // Pointer equality
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
for _, user := range users {
|
|
for _, workspace := range user.workspaces {
|
|
for _, app := range workspace.apps {
|
|
if usage.app == app { // Pointer equality
|
|
require.True(t, found, "test bug: app %q not in workspace %q: want user=%s workspace=%s; got user=%s workspace=%s ", usage.app.name, ws.name, ws.user.(*testUser).name, ws.name, user.name, workspace.name)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
require.True(t, found, "test bug: app %q not in workspace %q", usage.app.name, ws.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
return templates, users, testData
|
|
}
|
|
|
|
prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) (*codersdk.Client, chan dbrollup.Event) {
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
|
|
db, ps := dbtestutil.NewDB(t)
|
|
events := make(chan dbrollup.Event)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: ps,
|
|
Logger: &logger,
|
|
IncludeProvisionerDaemon: true,
|
|
AgentStatsRefreshInterval: time.Hour, // Not relevant for this test.
|
|
DatabaseRolluper: dbrollup.New(
|
|
logger.Named("dbrollup"),
|
|
db,
|
|
dbrollup.WithInterval(time.Millisecond*50),
|
|
dbrollup.WithEventChannel(events),
|
|
),
|
|
})
|
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Prepare all test users.
|
|
for _, user := range users {
|
|
_ = dbgen.User(t, db, database.User{
|
|
ID: user.id,
|
|
Username: user.name,
|
|
Status: database.UserStatusActive,
|
|
})
|
|
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
|
UserID: user.id,
|
|
OrganizationID: firstUser.OrganizationID,
|
|
})
|
|
token, err := client.CreateToken(context.Background(), user.id.String(), codersdk.CreateTokenRequest{
|
|
Lifetime: time.Hour * 24,
|
|
Scope: codersdk.APIKeyScopeAll,
|
|
TokenName: "no-password-user-token",
|
|
})
|
|
require.NoError(t, err)
|
|
userClient := codersdk.New(client.URL)
|
|
userClient.SetSessionToken(token.Key)
|
|
|
|
coderUser, err := userClient.User(context.Background(), user.id.String())
|
|
require.NoError(t, err)
|
|
|
|
user.client = userClient
|
|
user.sdk = coderUser
|
|
|
|
user.client.SetLogger(logger.Named("user").With(slog.Field{Name: "name", Value: user.name}))
|
|
}
|
|
|
|
// Prepare all the templates.
|
|
for _, template := range templates {
|
|
template := template
|
|
|
|
// Prepare all workspace resources (agents and apps).
|
|
var (
|
|
createWorkspaces []func(uuid.UUID)
|
|
waitWorkspaces []func()
|
|
)
|
|
var resources []*proto.Resource
|
|
for _, user := range users {
|
|
user := user
|
|
for _, workspace := range user.workspaces {
|
|
workspace := workspace
|
|
|
|
if workspace.template != template {
|
|
continue
|
|
}
|
|
authToken := uuid.New()
|
|
agentClient := agentsdk.New(client.URL)
|
|
agentClient.SetSessionToken(authToken.String())
|
|
workspace.agentClient = agentClient
|
|
|
|
var apps []*proto.App
|
|
for _, app := range workspace.apps {
|
|
apps = append(apps, &proto.App{
|
|
Slug: app.name,
|
|
DisplayName: app.name,
|
|
Icon: app.icon,
|
|
SharingLevel: proto.AppSharingLevel_OWNER,
|
|
Url: "http://",
|
|
})
|
|
}
|
|
|
|
resources = append(resources, &proto.Resource{
|
|
Name: "example",
|
|
Type: "aws_instance",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(), // Doesn't matter, not used in DB.
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{
|
|
Token: authToken.String(),
|
|
},
|
|
Apps: apps,
|
|
}},
|
|
})
|
|
|
|
createWorkspaces = append(createWorkspaces, func(templateID uuid.UUID) {
|
|
// Create workspace using the users client.
|
|
createdWorkspace := coderdtest.CreateWorkspace(t, user.client, firstUser.OrganizationID, templateID)
|
|
workspace.id = createdWorkspace.ID
|
|
waitWorkspaces = append(waitWorkspaces, func() {
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, user.client, createdWorkspace.LatestBuild.ID)
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
ws, err := user.client.Workspace(ctx, workspace.id)
|
|
require.NoError(t, err, "want no error getting workspace")
|
|
|
|
workspace.agentID = ws.LatestBuild.Resources[0].Agents[0].ID
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// Create the template version and template.
|
|
version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{
|
|
Resources: resources,
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
// Create template, essentially a modified version of CreateTemplate
|
|
// where we can control the template ID.
|
|
// createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
|
|
createdTemplate := dbgen.Template(t, db, database.Template{
|
|
ID: template.id,
|
|
ActiveVersionID: version.ID,
|
|
OrganizationID: firstUser.OrganizationID,
|
|
CreatedBy: firstUser.UserID,
|
|
GroupACL: database.TemplateACL{
|
|
firstUser.OrganizationID.String(): []rbac.Action{rbac.ActionRead},
|
|
},
|
|
})
|
|
err := db.UpdateTemplateVersionByID(context.Background(), database.UpdateTemplateVersionByIDParams{
|
|
ID: version.ID,
|
|
TemplateID: uuid.NullUUID{
|
|
UUID: createdTemplate.ID,
|
|
Valid: true,
|
|
},
|
|
})
|
|
require.NoError(t, err, "want no error updating template version")
|
|
|
|
// Create all workspaces and wait for them.
|
|
for _, createWorkspace := range createWorkspaces {
|
|
createWorkspace(template.id)
|
|
}
|
|
for _, waitWorkspace := range waitWorkspaces {
|
|
waitWorkspace()
|
|
}
|
|
}
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
|
|
// Use agent stats batcher to insert agent stats, similar to live system.
|
|
// NOTE(mafredri): Ideally we would pass batcher as a coderd option and
|
|
// insert using the agentClient, but we have a circular dependency on
|
|
// the database.
|
|
batcher, batcherCloser, err := batchstats.New(
|
|
ctx,
|
|
batchstats.WithStore(db),
|
|
batchstats.WithLogger(logger.Named("batchstats")),
|
|
batchstats.WithInterval(time.Hour),
|
|
)
|
|
require.NoError(t, err)
|
|
defer batcherCloser() // Flushes the stats, this is to ensure they're written.
|
|
|
|
for workspace, data := range testData {
|
|
for _, stat := range data.agentStats {
|
|
createdAt := stat.startedAt
|
|
connectionCount := int64(1)
|
|
if stat.noConnections {
|
|
connectionCount = 0
|
|
}
|
|
for createdAt.Before(stat.endedAt) {
|
|
err = batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{
|
|
ConnectionCount: connectionCount,
|
|
SessionCountVscode: stat.sessionCountVSCode,
|
|
SessionCountJetbrains: stat.sessionCountJetBrains,
|
|
SessionCountReconnectingPty: stat.sessionCountReconnectingPTY,
|
|
SessionCountSsh: stat.sessionCountSSH,
|
|
})
|
|
require.NoError(t, err, "want no error inserting agent stats")
|
|
createdAt = createdAt.Add(30 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Insert app usage.
|
|
var stats []workspaceapps.StatsReport
|
|
for workspace, data := range testData {
|
|
for _, usage := range data.appUsage {
|
|
appName := usage.app.name
|
|
accessMethod := workspaceapps.AccessMethodPath
|
|
if usage.app.name == "terminal" {
|
|
appName = ""
|
|
accessMethod = workspaceapps.AccessMethodTerminal
|
|
}
|
|
stats = append(stats, workspaceapps.StatsReport{
|
|
UserID: workspace.user.(*testUser).sdk.ID,
|
|
WorkspaceID: workspace.id,
|
|
AgentID: workspace.agentID,
|
|
AccessMethod: accessMethod,
|
|
SlugOrPort: appName,
|
|
SessionID: uuid.New(),
|
|
SessionStartedAt: usage.startedAt,
|
|
SessionEndedAt: usage.endedAt,
|
|
Requests: usage.requests,
|
|
})
|
|
}
|
|
}
|
|
reporter := workspaceapps.NewStatsDBReporter(db, workspaceapps.DefaultStatsDBReporterBatchSize)
|
|
//nolint:gocritic // This is a test.
|
|
err = reporter.Report(dbauthz.AsSystemRestricted(ctx), stats)
|
|
require.NoError(t, err, "want no error inserting app stats")
|
|
|
|
return client, events
|
|
}
|
|
|
|
baseTemplateAndUserFixture := func() ([]*testTemplate, []*testUser) {
|
|
// Test templates and configuration to generate.
|
|
templates := []*testTemplate{
|
|
// Create two templates with near-identical apps and parameters
|
|
// to allow testing for grouping similar data.
|
|
{
|
|
name: "template1",
|
|
apps: []templateApp{
|
|
{name: "app1", icon: "/icon1.png"},
|
|
{name: "app2", icon: "/icon2.png"},
|
|
{name: "app3", icon: "/icon2.png"},
|
|
},
|
|
},
|
|
{
|
|
name: "template2",
|
|
apps: []templateApp{
|
|
{name: "app1", icon: "/icon1.png"},
|
|
{name: "app2", icon: "/icon2.png"},
|
|
{name: "app3", icon: "/icon2.png"},
|
|
},
|
|
},
|
|
// Create another template with different parameters and apps.
|
|
{
|
|
name: "othertemplate",
|
|
apps: []templateApp{
|
|
{name: "otherapp1", icon: "/icon1.png"},
|
|
|
|
// This "special test app" will be converted into web
|
|
// terminal usage, this is not included in stats since we
|
|
// currently rely on agent stats for this data.
|
|
{name: "terminal", icon: "/terminal.png"},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Users and workspaces to generate.
|
|
users := []*testUser{
|
|
{
|
|
name: "user1",
|
|
workspaces: []*testWorkspace{
|
|
{
|
|
name: "workspace1",
|
|
template: templates[0],
|
|
},
|
|
{
|
|
name: "workspace2",
|
|
template: templates[1],
|
|
},
|
|
{
|
|
name: "otherworkspace3",
|
|
template: templates[2],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "user2",
|
|
workspaces: []*testWorkspace{
|
|
{
|
|
name: "workspace1",
|
|
template: templates[0],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "user3",
|
|
workspaces: []*testWorkspace{
|
|
{
|
|
name: "otherworkspace1",
|
|
template: templates[2],
|
|
},
|
|
{
|
|
name: "workspace2",
|
|
template: templates[0],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return templates, users
|
|
}
|
|
|
|
// Time range for report, test data will be generated within and
|
|
// outside this range, but only data within the range should be
|
|
// included in the report.
|
|
frozenLastNight := time.Date(2023, 8, 22, 0, 0, 0, 0, time.UTC)
|
|
frozenWeekAgo := frozenLastNight.AddDate(0, 0, -7)
|
|
|
|
saoPaulo, err := time.LoadLocation("America/Sao_Paulo")
|
|
require.NoError(t, err)
|
|
frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo)
|
|
require.NoError(t, err)
|
|
|
|
//nolint:dupl // For testing purposes
|
|
makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen {
|
|
return map[*testWorkspace]testDataGen{
|
|
users[0].workspaces[0]: {
|
|
agentStats: []agentStat{
|
|
{ // One hour of usage.
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
sessionCountVSCode: 1,
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // 12 minutes of usage.
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 1),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 1).Add(12 * time.Minute),
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // 1m30s of usage -> 2m rounded.
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(4*time.Minute + 30*time.Second),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Minute),
|
|
sessionCountJetBrains: 1,
|
|
},
|
|
},
|
|
appUsage: []appUsage{
|
|
{ // One hour of usage.
|
|
app: users[0].workspaces[0].apps[0],
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
requests: 1,
|
|
},
|
|
{ // 30s of app usage -> 1m rounded.
|
|
app: users[0].workspaces[0].apps[0],
|
|
startedAt: frozenWeekAgo.Add(2*time.Hour + 10*time.Second),
|
|
endedAt: frozenWeekAgo.Add(2*time.Hour + 40*time.Second),
|
|
requests: 1,
|
|
},
|
|
{ // 1m30s of app usage -> 2m rounded (included in São Paulo).
|
|
app: users[0].workspaces[0].apps[0],
|
|
startedAt: frozenWeekAgo.Add(3*time.Hour + 30*time.Second),
|
|
endedAt: frozenWeekAgo.Add(3*time.Hour + 90*time.Second),
|
|
requests: 1,
|
|
},
|
|
{ // used an app on the last day, counts as active user, 12m.
|
|
app: users[0].workspaces[0].apps[2],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 6),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 6).Add(12 * time.Minute),
|
|
requests: 1,
|
|
},
|
|
},
|
|
},
|
|
users[0].workspaces[1]: {
|
|
agentStats: []agentStat{
|
|
{
|
|
// One hour of usage in second template at the same time
|
|
// as in first template. When selecting both templates
|
|
// this user and their app usage will only be counted
|
|
// once but the template ID will show up in the data.
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
sessionCountVSCode: 1,
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // One hour of usage.
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, -12),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, -12).Add(time.Hour),
|
|
sessionCountSSH: 1,
|
|
sessionCountReconnectingPTY: 1,
|
|
},
|
|
{ // Another one hour of usage, but "active users" shouldn't be increased twice.
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, -10),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, -10).Add(time.Hour),
|
|
sessionCountSSH: 1,
|
|
sessionCountReconnectingPTY: 1,
|
|
},
|
|
},
|
|
appUsage: []appUsage{
|
|
{ // One hour of usage, but same user and same template app, only count once.
|
|
app: users[0].workspaces[1].apps[0],
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
requests: 1,
|
|
},
|
|
{
|
|
// Different templates but identical apps, apps will be
|
|
// combined and usage will be summed.
|
|
app: users[0].workspaces[1].apps[0],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 2),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Hour),
|
|
requests: 1,
|
|
},
|
|
},
|
|
},
|
|
users[0].workspaces[2]: {
|
|
agentStats: []agentStat{},
|
|
appUsage: []appUsage{},
|
|
},
|
|
users[1].workspaces[0]: {
|
|
agentStats: []agentStat{
|
|
{ // One hour of agent usage before timeframe (exclude).
|
|
startedAt: frozenWeekAgo.Add(-time.Hour),
|
|
endedAt: frozenWeekAgo,
|
|
sessionCountVSCode: 1,
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // One hour of usage.
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
sessionCountSSH: 1,
|
|
},
|
|
{ // One hour of agent usage after timeframe (exclude in UTC, include in São Paulo).
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 7),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 7).Add(time.Hour),
|
|
sessionCountVSCode: 1,
|
|
sessionCountSSH: 1,
|
|
},
|
|
},
|
|
appUsage: []appUsage{
|
|
{ // One hour of app usage before timeframe (exclude).
|
|
app: users[1].workspaces[0].apps[2],
|
|
startedAt: frozenWeekAgo.Add(-time.Hour),
|
|
endedAt: frozenWeekAgo,
|
|
requests: 1,
|
|
},
|
|
{ // One hour of app usage after timeframe (exclude in UTC, include in São Paulo).
|
|
app: users[1].workspaces[0].apps[2],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 7),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 7).Add(time.Hour),
|
|
requests: 1,
|
|
},
|
|
},
|
|
},
|
|
users[2].workspaces[0]: {
|
|
agentStats: []agentStat{
|
|
{ // One hour of usage.
|
|
startedAt: frozenWeekAgo,
|
|
endedAt: frozenWeekAgo.Add(time.Hour),
|
|
sessionCountSSH: 1,
|
|
sessionCountReconnectingPTY: 1,
|
|
},
|
|
},
|
|
appUsage: []appUsage{
|
|
{
|
|
app: users[2].workspaces[0].apps[0],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 2),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(5 * time.Minute),
|
|
requests: 1,
|
|
},
|
|
{ // Special app; excluded from apps, but counted as active during the day.
|
|
app: users[2].workspaces[0].apps[1],
|
|
startedAt: frozenWeekAgo.AddDate(0, 0, 3),
|
|
endedAt: frozenWeekAgo.AddDate(0, 0, 3).Add(5 * time.Minute),
|
|
requests: 1,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
type testRequest struct {
|
|
name string
|
|
makeRequest func([]*testTemplate) codersdk.UserActivityInsightsRequest
|
|
ignoreTimes bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
makeFixture func() ([]*testTemplate, []*testUser)
|
|
makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen
|
|
requests []testRequest
|
|
}{
|
|
{
|
|
name: "multiple users and workspaces",
|
|
makeFixture: baseTemplateAndUserFixture,
|
|
makeTestData: makeBaseTestData,
|
|
requests: []testRequest{
|
|
{
|
|
name: "week deployment wide",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "weekly aggregated deployment wide",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
StartTime: frozenWeekAgo.AddDate(0, 0, -3),
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 4),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "week all templates",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "weekly aggregated templates",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id},
|
|
StartTime: frozenWeekAgo.AddDate(0, 0, -1),
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 6),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "week first template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[0].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "weekly aggregated first template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[0].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "week second template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[1].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "three weeks second template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[1].id},
|
|
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "week third template",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
TemplateIDs: []uuid.UUID{templates[2].id},
|
|
StartTime: frozenWeekAgo,
|
|
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
// São Paulo is three hours behind UTC, so we should not see
|
|
// any data between weekAgo and weekAgo.Add(3 * time.Hour).
|
|
name: "week other timezone (São Paulo)",
|
|
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
|
|
return codersdk.UserActivityInsightsRequest{
|
|
StartTime: frozenWeekAgoSaoPaulo,
|
|
EndTime: frozenWeekAgoSaoPaulo.AddDate(0, 0, 7),
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.NotNil(t, tt.makeFixture, "test bug: makeFixture must be set")
|
|
require.NotNil(t, tt.makeTestData, "test bug: makeTestData must be set")
|
|
templates, users, testData := prepareFixtureAndTestData(t, tt.makeFixture, tt.makeTestData)
|
|
client, events := prepare(t, templates, users, testData)
|
|
|
|
// Drain two events, the first one resumes rolluper
|
|
// operation and the second one waits for the rollup
|
|
// to complete.
|
|
_, _ = <-events, <-events
|
|
|
|
for _, req := range tt.requests {
|
|
req := req
|
|
t.Run(req.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
report, err := client.UserActivityInsights(ctx, req.makeRequest(templates))
|
|
require.NoError(t, err, "want no error getting template insights")
|
|
|
|
if req.ignoreTimes {
|
|
// Ignore times, we're only interested in the data.
|
|
report.Report.StartTime = time.Time{}
|
|
report.Report.EndTime = time.Time{}
|
|
}
|
|
|
|
partialName := strings.Join(strings.Split(t.Name(), "/")[1:], "_")
|
|
goldenFile := filepath.Join("testdata", "insights", "user-activity", partialName+".json.golden")
|
|
if *updateGoldenFiles {
|
|
err = os.MkdirAll(filepath.Dir(goldenFile), 0o755)
|
|
require.NoError(t, err, "want no error creating golden file directory")
|
|
f, err := os.Create(goldenFile)
|
|
require.NoError(t, err, "want no error creating golden file")
|
|
defer f.Close()
|
|
enc := json.NewEncoder(f)
|
|
enc.SetIndent("", " ")
|
|
enc.Encode(report)
|
|
return
|
|
}
|
|
|
|
f, err := os.Open(goldenFile)
|
|
require.NoError(t, err, "open golden file, run \"make update-golden-files\" and commit the changes")
|
|
defer f.Close()
|
|
var want codersdk.UserActivityInsightsResponse
|
|
err = json.NewDecoder(f).Decode(&want)
|
|
require.NoError(t, err, "want no error decoding golden file")
|
|
|
|
cmpOpts := []cmp.Option{
|
|
// Ensure readable UUIDs in diff.
|
|
cmp.Transformer("UUIDs", func(in []uuid.UUID) (s []string) {
|
|
for _, id := range in {
|
|
s = append(s, id.String())
|
|
}
|
|
return s
|
|
}),
|
|
}
|
|
// Use cmp.Diff here because it produces more readable diffs.
|
|
assert.Empty(t, cmp.Diff(want, report, cmpOpts...), "golden file mismatch (-want +got): %s, run \"make update-golden-files\", verify and commit the changes", goldenFile)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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, -1),
|
|
EndTime: today,
|
|
Interval: "invalid",
|
|
})
|
|
assert.Error(t, err, "want error for bad interval")
|
|
|
|
_, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
|
|
StartTime: today.AddDate(0, 0, -5),
|
|
EndTime: today,
|
|
Interval: codersdk.InsightsReportIntervalWeek,
|
|
})
|
|
assert.Error(t, err, "last report interval must have at least 6 days")
|
|
|
|
_, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
|
|
StartTime: today.AddDate(0, 0, -1),
|
|
EndTime: today,
|
|
Interval: codersdk.InsightsReportIntervalWeek,
|
|
Sections: []codersdk.TemplateInsightsSection{"invalid"},
|
|
})
|
|
assert.Error(t, err, "want error for bad section")
|
|
}
|
|
|
|
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, nil)
|
|
owner := 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, owner.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, owner.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, nil)
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
|
|
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.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, owner.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, owner.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, nil)
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
|
|
regular, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
|
defer cancel()
|
|
|
|
var templateIDs []uuid.UUID
|
|
if tt.withTemplate {
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, owner.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 TestGenericInsights_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 fetchInsightsFunc func(ctx context.Context, client *codersdk.Client, startTime, endTime time.Time, templateIDs ...uuid.UUID) error
|
|
|
|
type test struct {
|
|
withTemplate bool
|
|
}
|
|
|
|
endpoints := map[string]fetchInsightsFunc{
|
|
"UserLatency": func(ctx context.Context, client *codersdk.Client, startTime, endTime time.Time, templateIDs ...uuid.UUID) error {
|
|
_, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{
|
|
StartTime: startTime,
|
|
EndTime: endTime,
|
|
TemplateIDs: templateIDs,
|
|
})
|
|
return err
|
|
},
|
|
"UserActivity": func(ctx context.Context, client *codersdk.Client, startTime, endTime time.Time, templateIDs ...uuid.UUID) error {
|
|
_, err := client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{
|
|
StartTime: startTime,
|
|
EndTime: endTime,
|
|
TemplateIDs: templateIDs,
|
|
})
|
|
return err
|
|
},
|
|
}
|
|
|
|
for endpointName, endpoint := range endpoints {
|
|
endpointName := endpointName
|
|
endpoint := endpoint
|
|
|
|
t.Run(fmt.Sprintf("With%sEndpoint", endpointName), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []test{
|
|
{true},
|
|
{false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
|
|
t.Run("AsOwner", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
owner := 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, owner.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
|
templateIDs = append(templateIDs, template.ID)
|
|
}
|
|
|
|
err := endpoint(ctx, client,
|
|
today,
|
|
time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour.
|
|
templateIDs...)
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("AsTemplateAdmin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
|
|
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.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, owner.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
|
templateIDs = append(templateIDs, template.ID)
|
|
}
|
|
|
|
err := endpoint(ctx, templateAdmin,
|
|
today,
|
|
time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour.
|
|
templateIDs...)
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("AsRegularUser", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
|
|
regular, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
|
defer cancel()
|
|
|
|
var templateIDs []uuid.UUID
|
|
if tt.withTemplate {
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
|
templateIDs = append(templateIDs, template.ID)
|
|
}
|
|
|
|
err := endpoint(ctx, regular,
|
|
today,
|
|
time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour.
|
|
templateIDs...)
|
|
require.Error(t, err)
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|