mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
Adds a database trigger that runs on insert and update of the `workspace_agents` table. The trigger ensures that the agent name is unique within the context of the workspace build it is being inserted into.
2373 lines
77 KiB
Go
2373 lines
77 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/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
|
"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/coderd/workspacestats"
|
|
"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 := testutil.Logger(t)
|
|
rollupEvents := make(chan dbrollup.Event)
|
|
statsInterval := 500 * time.Millisecond
|
|
// Speed up the test by controlling batch size and interval.
|
|
batcher, closeBatcher, err := workspacestats.NewBatcher(context.Background(),
|
|
workspacestats.BatcherWithLogger(logger.Named("batcher").Leveled(slog.LevelDebug)),
|
|
workspacestats.BatcherWithStore(db),
|
|
workspacestats.BatcherWithBatchSize(1),
|
|
workspacestats.BatcherWithInterval(statsInterval),
|
|
)
|
|
require.NoError(t, err)
|
|
defer closeBatcher()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: ps,
|
|
Logger: &logger,
|
|
IncludeProvisionerDaemon: true,
|
|
AgentStatsRefreshInterval: statsInterval,
|
|
StatsBatcher: batcher,
|
|
DatabaseRolluper: dbrollup.New(
|
|
logger.Named("dbrollup").Leveled(slog.LevelDebug),
|
|
db,
|
|
dbrollup.WithInterval(statsInterval/2),
|
|
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, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
|
|
// 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: testutil.Logger(t).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)
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
require.Fail(t, "timed out waiting for initial rollup event", ctx.Err())
|
|
case ev := <-rollupEvents:
|
|
require.True(t, ev.Init, "want init event")
|
|
}
|
|
|
|
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
|
|
}
|
|
t.Logf("waiting for deployment daus to update: %+v", daus)
|
|
}
|
|
}
|
|
|
|
func TestUserActivityInsights_SanityCheck(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, ps := dbtestutil.NewDB(t)
|
|
logger := testutil.Logger(t)
|
|
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, 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, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
|
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, 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 := testutil.Logger(t)
|
|
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.CreateUserRequestWithOrgs) {
|
|
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: fmt.Sprintf("dev-%d", len(resources)), // Ensure unique name per agent
|
|
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, 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(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
|
|
},
|
|
})
|
|
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 := workspacestats.NewBatcher(
|
|
ctx,
|
|
workspacestats.BatcherWithStore(db),
|
|
workspacestats.BatcherWithLogger(logger.Named("batchstats")),
|
|
workspacestats.BatcherWithInterval(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) {
|
|
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,
|
|
}, false)
|
|
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 := workspacestats.NewReporter(workspacestats.ReporterOptions{
|
|
Database: db,
|
|
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
|
|
})
|
|
//nolint:gocritic // This is a test.
|
|
err = reporter.ReportAppStats(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 gen/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 gen/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 := testutil.Logger(t)
|
|
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: fmt.Sprintf("dev-%d", len(resources)), // Ensure unique name per agent
|
|
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, 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(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
|
|
},
|
|
})
|
|
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 := workspacestats.NewBatcher(
|
|
ctx,
|
|
workspacestats.BatcherWithStore(db),
|
|
workspacestats.BatcherWithLogger(logger.Named("batchstats")),
|
|
workspacestats.BatcherWithInterval(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) {
|
|
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,
|
|
}, false)
|
|
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 := workspacestats.NewReporter(workspacestats.ReporterOptions{
|
|
Database: db,
|
|
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
|
|
})
|
|
//nolint:gocritic // This is a test.
|
|
err = reporter.ReportAppStats(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 gen/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 gen/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())
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|