package coderd import ( "context" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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 { tt := tt 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 sydneyLoc, err := time.LoadLocation("Australia/Sydney") // Random location require.NoError(t, err) now := time.Now().In(sydneyLoc) t.Logf("now: %s", now) y, m, d := now.Date() today := time.Date(y, m, d, 0, 0, 0, 0, sydneyLoc) t.Logf("today: %s", today) thisHour := time.Date(y, m, d, now.Hour(), 0, 0, 0, sydneyLoc) 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, }, { 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 { tt := tt 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 { tc := tc 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()) }