Files
coder/enterprise/coderd/usage/cron_test.go
T

109 lines
3.4 KiB
Go

package usage_test
import (
"context"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/usage/usagetypes"
"github.com/coder/coder/v2/enterprise/coderd/usage"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func TestCron(t *testing.T) {
t.Parallel()
t.Run("BasicTick", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)
clock := quartz.NewMock(t)
// The existence check should return false so the event gets
// inserted.
db.EXPECT().UsageEventExistsByID(gomock.Any(), gomock.Any()).
Return(false, nil).AnyTimes()
inserted := make(chan database.InsertUsageEventParams, 1)
db.EXPECT().InsertUsageEvent(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, params database.InsertUsageEventParams) error {
inserted <- params
return nil
}).AnyTimes()
inserter := usage.NewDBInserter(usage.InserterWithClock(clock))
cron := usage.NewCron(clock, slogtest.Make(t, nil), db, inserter)
require.NoError(t, cron.Register(usage.CronJob{
Name: "test-job",
Interval: 5 * time.Minute,
EventType: usagetypes.UsageEventTypeHBAISeatsV1,
Fn: func(_ context.Context) (usagetypes.HeartbeatEvent, error) {
return usagetypes.HBAISeats{Count: 42}, nil
},
}))
timerTrap := clock.Trap().NewTimer("test-job")
cron.Start(ctx)
defer cron.Close()
defer timerTrap.Close()
// Wait for timer creation, then fire it. The delay is the
// time until the next epoch-aligned boundary for the 5-minute
// interval — we don't assert the exact value since it depends
// on the mock clock's current time.
timerCall := timerTrap.MustWait(ctx)
timerCall.MustRelease(ctx)
clock.Advance(timerCall.Duration)
// Verify the event was inserted with an epoch-aligned ID.
select {
case params := <-inserted:
assert.Contains(t, params.ID, "hb_ai_seats_v1:")
case <-ctx.Done():
t.Fatal("timed out waiting for insert")
}
})
}
// TestAISeatsHeartbeat checks that AISeatsHeartbeat returns the
// correct event type and count. It wraps a mock database with dbauthz
// to verify that the AsUsagePublisher subject has the required
// ResourceAiSeat.ActionRead permission.
func TestAISeatsHeartbeat(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)
db.EXPECT().Wrappers().Return([]string{}).AnyTimes()
db.EXPECT().GetActiveAISeatCount(gomock.Any()).Return(int64(42), nil)
authz := rbac.NewStrictAuthorizer(prometheus.NewRegistry())
authzDB := dbauthz.New(db, authz, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
// AISeatsHeartbeat internally uses AsUsagePublisher, which must
// have ResourceAiSeat.ActionRead to pass the dbauthz check.
fn := usage.AISeatsHeartbeat(authzDB)
event, err := fn(testutil.Context(t, testutil.WaitLong))
require.NoError(t, err)
hb, ok := event.(usagetypes.HBAISeats)
require.True(t, ok)
assert.Equal(t, int64(42), hb.Count)
}