Ctrl K
ComunidadGuías…

coderd/insights_internal_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package coderd

import (
	"context"
	"encoding/json"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/codersdk"
)

func Test_parseInsightsStartAndEndTime(t *testing.T) {
	t.Parallel()

	t.Logf("machine location: %s", time.Now().Location())
	layout := insightsTimeLayout
	now := time.Now().UTC()
	t.Logf("now: %s", now)
	t.Logf("now location: %s", now.Location())
	y, m, d := now.Date()
	today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
	t.Logf("today: %s", today)
	thisHour := time.Date(y, m, d, now.Hour(), 0, 0, 0, time.UTC)
	t.Logf("thisHour: %s", thisHour)
	thisHourRoundUp := thisHour.Add(time.Hour)
	t.Logf("thisHourRoundUp: %s", thisHourRoundUp)

	helsinki, err := time.LoadLocation("Europe/Helsinki")
	require.NoError(t, err)

	type args struct {
		startTime string
		endTime   string
	}
	tests := []struct {
		name          string
		args          args
		wantStartTime time.Time
		wantEndTime   time.Time
		wantOk        bool
	}{
		{
			name: "Same",
			args: args{
				startTime: "2023-07-10T00:00:00Z",
				endTime:   "2023-07-10T00:00:00Z",
			},
			wantStartTime: time.Date(2023, 7, 10, 0, 0, 0, 0, time.UTC),
			wantEndTime:   time.Date(2023, 7, 10, 0, 0, 0, 0, time.UTC),
			wantOk:        true,
		},
		{
			name: "Week",
			args: args{
				startTime: "2023-07-10T00:00:00Z",
				endTime:   "2023-07-17T00:00:00Z",
			},
			wantStartTime: time.Date(2023, 7, 10, 0, 0, 0, 0, time.UTC),
			wantEndTime:   time.Date(2023, 7, 17, 0, 0, 0, 0, time.UTC),
			wantOk:        true,
		},
		{
			name: "Today",
			args: args{
				startTime: today.Format(layout),
				endTime:   thisHour.Format(layout),
			},
			wantStartTime: time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC),
			wantEndTime:   time.Date(today.Year(), today.Month(), today.Day(), thisHour.Hour(), 0, 0, 0, time.UTC),
			wantOk:        true,
		},
		{
			name: "Today with minutes and seconds",
			args: args{
				startTime: today.Format(layout),
				endTime:   thisHour.Add(time.Minute + time.Second).Format(layout),
			},
			wantOk: false,
		},
		{
			name: "Today hour round up",
			args: args{
				startTime: today.Format(layout),
				endTime:   thisHourRoundUp.Format(layout),
			},
			wantStartTime: time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC),
			wantEndTime:   time.Date(thisHourRoundUp.Year(), thisHourRoundUp.Month(), thisHourRoundUp.Day(), thisHourRoundUp.Hour(), 0, 0, 0, time.UTC),
			wantOk:        true,
		},
		{
			name: "Other timezone week",
			args: args{
				startTime: "2023-07-10T00:00:00+03:00",
				endTime:   "2023-07-17T00:00:00+03:00",
			},
			wantStartTime: time.Date(2023, 7, 10, 0, 0, 0, 0, helsinki),
			wantEndTime:   time.Date(2023, 7, 17, 0, 0, 0, 0, helsinki),
			wantOk:        true,
		},
		{
			name: "Daylight savings time",
			args: args{
				startTime: "2023-03-26T00:00:00+02:00",
				endTime:   "2023-03-27T00:00:00+03:00",
			},
			wantStartTime: time.Date(2023, 3, 26, 0, 0, 0, 0, helsinki),
			wantEndTime:   time.Date(2023, 3, 27, 0, 0, 0, 0, helsinki),
			wantOk:        true,
		},
		{
			name: "Bad format",
			args: args{
				startTime: "2023-07-10",
				endTime:   "2023-07-17",
			},
			wantOk: false,
		},
		{
			name: "Zero time",
			args: args{
				startTime: (time.Time{}).Format(layout),
				endTime:   (time.Time{}).Format(layout),
			},
			wantOk: false,
		},
		{
			name: "Time in future",
			args: args{
				startTime: today.AddDate(0, 0, 1).Format(layout),
				endTime:   today.AddDate(0, 0, 2).Format(layout),
			},
			wantOk: false,
		},
		{
			name: "End before start",
			args: args{
				startTime: today.Format(layout),
				endTime:   today.AddDate(0, 0, -1).Format(layout),
			},
			wantOk: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			t.Log("startTime: ", tt.args.startTime)
			t.Log("endTime: ", tt.args.endTime)
			if tt.wantOk {
				t.Log("wantStartTime: ", tt.wantStartTime)
				t.Log("wantEndTime: ", tt.wantEndTime)
			}

			rw := httptest.NewRecorder()
			gotStartTime, gotEndTime, gotOk := parseInsightsStartAndEndTime(context.Background(), rw, now, tt.args.startTime, tt.args.endTime)

			if !assert.Equal(t, tt.wantOk, gotOk) {
				//nolint:bodyclose
				t.Log("Status: ", rw.Result().StatusCode)
				t.Log("Body: ", rw.Body.String())
				return
			}
			// assert.Equal is unable to test time equality with different
			// (but same) locations because the *time.Location names differ
			// between LoadLocation and Parse, so we use assert.WithinDuration.
			assert.WithinDuration(t, tt.wantStartTime, gotStartTime, 0)
			assert.True(t, tt.wantStartTime.Equal(gotStartTime))
			assert.WithinDuration(t, tt.wantEndTime, gotEndTime, 0)
			assert.True(t, tt.wantEndTime.Equal(gotEndTime))
		})
	}
}

func Test_parseInsightsInterval_week(t *testing.T) {
	t.Parallel()

	layout := insightsTimeLayout
	losAngelesLoc, err := time.LoadLocation("America/Los_Angeles") // Random location
	require.NoError(t, err)

	now := time.Now().In(losAngelesLoc)
	t.Logf("now: %s", now)

	y, m, d := now.Date()
	today := time.Date(y, m, d, 0, 0, 0, 0, losAngelesLoc)
	t.Logf("today: %s", today)

	thisHour := time.Date(y, m, d, now.Hour(), 0, 0, 0, losAngelesLoc)
	t.Logf("thisHour: %s", thisHour)
	twoHoursAgo := thisHour.Add(-2 * time.Hour)
	t.Logf("twoHoursAgo: %s", twoHoursAgo)
	thirteenDaysAgo := today.AddDate(0, 0, -13)
	t.Logf("thirteenDaysAgo: %s", thirteenDaysAgo)

	sixDaysAgo := today.AddDate(0, 0, -6)
	t.Logf("sixDaysAgo: %s", sixDaysAgo)
	nineDaysAgo := today.AddDate(0, 0, -9)
	t.Logf("nineDaysAgo: %s", nineDaysAgo)

	type args struct {
		startTime string
		endTime   string
	}
	tests := []struct {
		name   string
		args   args
		wantOk bool
	}{
		{
			name: "Two full weeks",
			args: args{
				startTime: "2023-08-10T00:00:00+02:00",
				endTime:   "2023-08-24T00:00:00+02:00",
			},
			wantOk: true,
		},
		{
			name: "One full week",
			args: args{
				startTime: "2023-09-06T00:00:00+02:00",
				endTime:   "2023-09-13T00:00:00+02:00",
			},
			wantOk: true,
		},
		/* FIXME: daylight savings issue
		{
			name: "6 days are acceptable",
			args: args{
				startTime: sixDaysAgo.Format(layout),
				endTime:   stripTime(thisHour).Format(layout),
			},
			wantOk: true,
		},*/
		{
			name: "Shorter than a full week",
			args: args{
				startTime: "2023-09-08T00:00:00+02:00",
				endTime:   "2023-09-13T00:00:00+02:00",
			},
			wantOk: false,
		},
		{
			name: "9 days (7 + 2) are not acceptable",
			args: args{
				startTime: nineDaysAgo.Format(layout),
				endTime:   stripTime(thisHour).Format(layout),
			},
			wantOk: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			t.Log("startTime: ", tt.args.startTime)
			t.Log("endTime: ", tt.args.endTime)

			rw := httptest.NewRecorder()
			startTime, endTime, ok := parseInsightsStartAndEndTime(context.Background(), rw, now, tt.args.startTime, tt.args.endTime)
			if !ok {
				//nolint:bodyclose
				t.Log("Status: ", rw.Result().StatusCode)
				t.Log("Body: ", rw.Body.String())
			}
			require.True(t, ok, "start_time and end_time must be valid")

			parsedInterval, gotOk := parseInsightsInterval(context.Background(), rw, "week", startTime, endTime)
			if !assert.Equal(t, tt.wantOk, gotOk) {
				//nolint:bodyclose
				t.Log("Status: ", rw.Result().StatusCode)
				t.Log("Body: ", rw.Body.String())
				return
			}
			if tt.wantOk {
				assert.Equal(t, codersdk.InsightsReportIntervalWeek, parsedInterval)
			}
		})
	}
}

func TestLastReportIntervalHasAtLeastSixDays(t *testing.T) {
	t.Parallel()

	loc, err := time.LoadLocation("Europe/Warsaw")
	require.NoError(t, err)

	testCases := []struct {
		name      string
		startTime time.Time
		endTime   time.Time
		expected  bool
	}{
		{
			name:      "perfectly full week",
			startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, loc),
			endTime:   time.Date(2023, time.September, 18, 12, 0, 0, 0, loc),
			expected:  true,
		},
		{
			name:      "exactly 6 days apart",
			startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, loc),
			endTime:   time.Date(2023, time.September, 17, 12, 0, 0, 0, loc),
			expected:  true,
		},
		{
			name:      "less than 6 days apart",
			startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, time.UTC),
			endTime:   time.Date(2023, time.September, 17, 11, 0, 0, 0, time.UTC),
			expected:  false,
		},
		{
			name:      "forward DST change, 5 days and 23 hours apart",
			startTime: time.Date(2023, time.March, 22, 12, 0, 0, 0, loc), // A day before DST starts
			endTime:   time.Date(2023, time.March, 28, 12, 0, 0, 0, loc), // Exactly 6 "days" apart
			expected:  true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			t.Log("startTime: ", tc.startTime)
			t.Log("endTime: ", tc.endTime)

			result := lastReportIntervalHasAtLeastSixDays(tc.startTime, tc.endTime)
			if result != tc.expected {
				t.Errorf("Expected %v, but got %v for start time %v and end time %v", tc.expected, result, tc.startTime, tc.endTime)
			}
		})
	}
}

// stripTime strips the time from a time.Time value, but keeps the date and TZ.
func stripTime(t time.Time) time.Time {
	y, m, d := t.Date()
	return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
}

func TestConvertTemplateInsightsApps(t *testing.T) {
	t.Parallel()

	t.Run("ListsEachApp", func(t *testing.T) {
		t.Parallel()

		sharedTemplateID, cursorTemplateID := uuid.New(), uuid.New()
		apps, err := convertTemplateInsightsApps(database.GetTemplateInsightsRow{
			SessionAppUsageSeconds: json.RawMessage(`{"vscode": 300, "cursor": 120, "some_new_ide": 60, "overflow": 120, "unknown": 60, "reconnecting_pty": 90, "port_forwarding": 30}`),
			SessionAppTemplateIds: json.RawMessage(`{
				"vscode": ["` + sharedTemplateID.String() + `"],
				"cursor": ["` + sharedTemplateID.String() + `", "` + cursorTemplateID.String() + `"],
				"some_new_ide": ["` + cursorTemplateID.String() + `"]
			}`),
		}, []database.GetTemplateAppInsightsRow{{
			TemplateIDs:  []uuid.UUID{sharedTemplateID},
			Slug:         "filebrowser",
			DisplayName:  "File Browser",
			UsageSeconds: 30,
		}})
		require.NoError(t, err)
		require.Equal(t, []codersdk.TemplateAppUsage{
			{
				TemplateIDs: []uuid.UUID{sharedTemplateID, cursorTemplateID},
				Method:      codersdk.ConnectionLogMethodSSH,
				DisplayName: "Cursor",
				Slug:        "cursor",
				Icon:        "/icon/cursor.svg",
				Seconds:     120,
			},
			{
				TemplateIDs: []uuid.UUID{},
				Method:      codersdk.ConnectionLogMethodSSH,
				DisplayName: "Other apps",
				Slug:        codersdk.AppNameOverflow,
				Seconds:     120,
			},
			{
				TemplateIDs: []uuid.UUID{},
				Method:      codersdk.ConnectionLogMethodPortForwarding,
				DisplayName: "Port Forwarding",
				Slug:        "port_forwarding",
				Seconds:     30,
			},
			{
				TemplateIDs: []uuid.UUID{},
				Method:      codersdk.ConnectionLogMethodReconnectingPTY,
				DisplayName: "Web Terminal",
				Slug:        "reconnecting_pty",
				Icon:        "/icon/terminal.svg",
				Seconds:     90,
			},
			{
				TemplateIDs: []uuid.UUID{cursorTemplateID},
				Method:      codersdk.ConnectionLogMethodSSH,
				DisplayName: "some_new_ide",
				Slug:        "some_new_ide",
				Seconds:     60,
			},
			{
				TemplateIDs: []uuid.UUID{},
				Method:      codersdk.ConnectionLogMethodSSH,
				DisplayName: "Unknown",
				Slug:        "unknown",
				Seconds:     60,
			},
			{
				TemplateIDs: []uuid.UUID{sharedTemplateID},
				Method:      codersdk.ConnectionLogMethodSSH,
				DisplayName: "VS Code",
				Slug:        "vscode",
				Icon:        "/icon/code.svg",
				Seconds:     300,
			},
			{
				TemplateIDs: []uuid.UUID{sharedTemplateID},
				Method:      codersdk.ConnectionLogMethodWorkspaceApp,
				DisplayName: "File Browser",
				Slug:        "filebrowser",
				Seconds:     30,
			},
		}, apps)
	})

	t.Run("FoldedOverflowIsUpperBound", func(t *testing.T) {
		t.Parallel()

		// Only the session overflow row carries the folded apps, so only it
		// is an upper bound. A template app may still use overflow as its
		// slug, and it is not an accounting row.
		templateID := uuid.New()
		apps, err := convertTemplateInsightsApps(database.GetTemplateInsightsRow{
			SessionAppUsageSeconds: json.RawMessage(`{"overflow": 120, "unknown": 60, "future_ide": 30}`),
			SessionAppTemplateIds:  json.RawMessage(`{}`),
			SessionFoldedAppCount:  2,
		}, []database.GetTemplateAppInsightsRow{{
			TemplateIDs:  []uuid.UUID{templateID},
			Slug:         codersdk.AppNameOverflow,
			DisplayName:  "Overflow",
			UsageSeconds: 60,
		}})
		require.NoError(t, err)
		upperBound := map[string]bool{}
		for _, app := range apps {
			upperBound[string(app.Method)+"/"+app.Slug] = app.SecondsIsUpperBound
		}
		require.Equal(t, map[string]bool{
			"ssh/future_ide":         false,
			"ssh/overflow":           true,
			"ssh/unknown":            false,
			"workspace_app/overflow": false,
		}, upperBound)
	})

	t.Run("Malformed", func(t *testing.T) {
		t.Parallel()

		for name, usage := range map[string]database.GetTemplateInsightsRow{
			"UsageSeconds": {
				SessionAppUsageSeconds: json.RawMessage(`{"vscode": {}}`),
				SessionAppTemplateIds:  json.RawMessage(`{}`),
			},
			"TemplateIDs": {
				SessionAppUsageSeconds: json.RawMessage(`{}`),
				SessionAppTemplateIds:  json.RawMessage(`{"vscode": "not-a-uuid"}`),
			},
		} {
			t.Run(name, func(t *testing.T) {
				t.Parallel()

				apps, err := convertTemplateInsightsApps(usage, nil)
				require.ErrorContains(t, err, "decode session app")
				require.Nil(t, apps)
			})
		}
	})
}