zitadel/internal/serviceping/worker_test.go
Livio Spring 82cd1cee08
fix(service ping): correct endpoint, validate and randomize default interval (#10166)
# Which Problems Are Solved

The production endpoint of the service ping was wrong.
Additionally we discussed in the sprint review, that we could randomize
the default interval to prevent all systems to report data at the very
same time and also require a minimal interval.

# How the Problems Are Solved

- fixed the endpoint
- If the interval is set to @daily (default), we generate a random time
(minute, hour) as a cron format.
- Check if the interval is more than 30min and return an error if not.
- Fixed yaml indent on `ResourceCount`

# Additional Changes

None

# Additional Context

as discussed internally
2025-07-04 13:45:15 +00:00

1127 lines
31 KiB
Go

package serviceping
import (
"context"
"fmt"
"net/http"
"reflect"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/riverqueue/river"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/queue"
"github.com/zitadel/zitadel/internal/serviceping/mock"
"github.com/zitadel/zitadel/internal/zerrors"
analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta"
)
var (
testNow = time.Now()
errInsert = fmt.Errorf("insert error")
)
func TestWorker_reportBaseInformation(t *testing.T) {
type fields struct {
reportClient func(*testing.T) analytics.TelemetryServiceClient
db func(*testing.T) Queries
systemID string
version string
}
type args struct {
ctx context.Context
}
type want struct {
reportID string
err error
}
tests := []struct {
name string
fields fields
args args
want want
}{
{
name: "database error, error",
fields: fields{
db: func(t *testing.T) Queries {
ctrl := gomock.NewController(t)
queries := mock.NewMockQueries(ctrl)
queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return(
nil, zerrors.ThrowInternal(nil, "id", "db error"),
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
return mock.NewMockTelemetryServiceClient(gomock.NewController(t))
},
},
want: want{
reportID: "",
err: zerrors.ThrowInternal(nil, "id", "db error"),
},
},
{
name: "telemetry client error, error",
fields: fields{
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{
SystemId: "system-id",
Version: "version",
Instances: []*analytics.InstanceInformation{
{
Id: "id",
Domains: []string{"domain", "domain2"},
CreatedAt: timestamppb.New(testNow),
},
},
}).Return(
nil, status.Error(codes.Internal, "error"),
)
return client
},
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return(
&query.Instances{
Instances: []*query.Instance{
{
ID: "id",
CreationDate: testNow,
Domains: []*query.InstanceDomain{
{
Domain: "domain",
},
{
Domain: "domain2",
},
},
},
},
},
nil,
)
return queries
},
systemID: "system-id",
version: "version",
},
want: want{
reportID: "",
err: status.Error(codes.Internal, "error"),
},
},
{
name: "report ok, reportID returned",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return(
&query.Instances{
Instances: []*query.Instance{
{
ID: "id",
CreationDate: testNow,
Domains: []*query.InstanceDomain{
{
Domain: "domain",
},
{
Domain: "domain2",
},
},
},
},
},
nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{
SystemId: "system-id",
Version: "version",
Instances: []*analytics.InstanceInformation{
{
Id: "id",
Domains: []string{"domain", "domain2"},
CreatedAt: timestamppb.New(testNow),
},
},
}).Return(
&analytics.ReportBaseInformationResponse{ReportId: "report-id"}, nil,
)
return client
},
systemID: "system-id",
version: "version",
},
want: want{
reportID: "report-id",
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &Worker{
reportClient: tt.fields.reportClient(t),
db: tt.fields.db(t),
systemID: tt.fields.systemID,
version: tt.fields.version,
}
got, err := w.reportBaseInformation(tt.args.ctx)
assert.Equal(t, tt.want.reportID, got)
assert.ErrorIs(t, err, tt.want.err)
})
}
}
func TestWorker_reportResourceCounts(t *testing.T) {
type fields struct {
reportClient func(*testing.T) analytics.TelemetryServiceClient
db func(*testing.T) Queries
config *Config
systemID string
}
type args struct {
ctx context.Context
reportID string
}
tests := []struct {
name string
fields fields
args args
wantErr error
}{
{
name: "database error, error",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 1).Return(
nil, zerrors.ThrowInternal(nil, "id", "db error"),
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
return mock.NewMockTelemetryServiceClient(gomock.NewController(t))
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
BulkSize: 1,
},
},
},
systemID: "system-id",
},
args: args{
ctx: context.Background(),
reportID: "",
},
wantErr: zerrors.ThrowInternal(nil, "id", "db error"),
},
{
name: "no resource counts, no error",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 1).Return(
[]query.ResourceCount{}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
return mock.NewMockTelemetryServiceClient(gomock.NewController(t))
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
BulkSize: 1,
},
},
},
systemID: "system-id",
},
args: args{
ctx: context.Background(),
reportID: "",
},
wantErr: nil,
},
{
name: "telemetry client error, error",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return(
[]query.ResourceCount{
{
ID: 1,
InstanceID: "instance-id",
TableName: "table_name",
ParentType: domain.CountParentTypeInstance,
ParentID: "instance-id",
Resource: "resource",
UpdatedAt: testNow,
Amount: 10,
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{
SystemId: "system-id",
ReportId: nil,
ResourceCounts: []*analytics.ResourceCount{
{
InstanceId: "instance-id",
TableName: "table_name",
ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE,
ParentId: "instance-id",
ResourceName: "resource",
UpdatedAt: timestamppb.New(testNow),
Amount: 10,
},
},
}).Return(
nil, status.Error(codes.Internal, "error"),
)
return client
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
BulkSize: 2,
},
},
},
systemID: "system-id",
},
args: args{
ctx: context.Background(),
reportID: "",
},
wantErr: status.Error(codes.Internal, "error"),
},
{
name: "report ok, no additional counts, no error",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return(
[]query.ResourceCount{
{
ID: 1,
InstanceID: "instance-id",
TableName: "table_name",
ParentType: domain.CountParentTypeInstance,
ParentID: "instance-id",
Resource: "resource",
UpdatedAt: testNow,
Amount: 10,
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{
SystemId: "system-id",
ReportId: nil,
ResourceCounts: []*analytics.ResourceCount{
{
InstanceId: "instance-id",
TableName: "table_name",
ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE,
ParentId: "instance-id",
ResourceName: "resource",
UpdatedAt: timestamppb.New(testNow),
Amount: 10,
},
},
}).Return(
&analytics.ReportResourceCountsResponse{
ReportId: "report-id",
}, nil,
)
return client
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
BulkSize: 2,
},
},
},
systemID: "system-id",
},
args: args{
ctx: context.Background(),
reportID: "",
},
wantErr: nil,
},
{
name: "report ok, additional counts, no error",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return(
[]query.ResourceCount{
{
ID: 1,
InstanceID: "instance-id",
TableName: "table_name",
ParentType: domain.CountParentTypeInstance,
ParentID: "instance-id",
Resource: "resource",
UpdatedAt: testNow,
Amount: 10,
},
{
ID: 2,
InstanceID: "instance-id2",
TableName: "table_name",
ParentType: domain.CountParentTypeInstance,
ParentID: "instance-id2",
Resource: "resource",
UpdatedAt: testNow,
Amount: 5,
},
}, nil,
)
queries.EXPECT().ListResourceCounts(gomock.Any(), 2, 2).Return(
[]query.ResourceCount{
{
ID: 3,
InstanceID: "instance-id3",
TableName: "table_name",
ParentType: domain.CountParentTypeInstance,
ParentID: "instance-id3",
Resource: "resource",
UpdatedAt: testNow,
Amount: 20,
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{
SystemId: "system-id",
ReportId: nil,
ResourceCounts: []*analytics.ResourceCount{
{
InstanceId: "instance-id",
TableName: "table_name",
ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE,
ParentId: "instance-id",
ResourceName: "resource",
UpdatedAt: timestamppb.New(testNow),
Amount: 10,
},
{
InstanceId: "instance-id2",
TableName: "table_name",
ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE,
ParentId: "instance-id2",
ResourceName: "resource",
UpdatedAt: timestamppb.New(testNow),
Amount: 5,
},
},
}).Return(
&analytics.ReportResourceCountsResponse{
ReportId: "report-id",
}, nil,
)
client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{
SystemId: "system-id",
ReportId: gu.Ptr("report-id"),
ResourceCounts: []*analytics.ResourceCount{
{
InstanceId: "instance-id3",
TableName: "table_name",
ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE,
ParentId: "instance-id3",
ResourceName: "resource",
UpdatedAt: timestamppb.New(testNow),
Amount: 20,
},
},
}).Return(
&analytics.ReportResourceCountsResponse{
ReportId: "report-id",
}, nil,
)
return client
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
BulkSize: 2,
},
},
},
systemID: "system-id",
},
args: args{
ctx: context.Background(),
reportID: "",
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &Worker{
reportClient: tt.fields.reportClient(t),
db: tt.fields.db(t),
config: tt.fields.config,
systemID: tt.fields.systemID,
}
err := w.reportResourceCounts(tt.args.ctx, tt.args.reportID)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestWorker_Work(t *testing.T) {
type fields struct {
WorkerDefaults river.WorkerDefaults[*ServicePingReport]
reportClient func(*testing.T) analytics.TelemetryServiceClient
db func(*testing.T) Queries
queue func(*testing.T) Queue
config *Config
systemID string
version string
}
type args struct {
ctx context.Context
job *river.Job[*ServicePingReport]
}
tests := []struct {
name string
fields fields
args args
wantErr error
}{
{
name: "unknown report type, cancel job",
fields: fields{
db: func(t *testing.T) Queries {
return mock.NewMockQueries(gomock.NewController(t))
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
return mock.NewMockTelemetryServiceClient(gomock.NewController(t))
},
queue: func(t *testing.T) Queue {
return mock.NewMockQueue(gomock.NewController(t))
},
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportType: 100000,
},
},
},
wantErr: river.JobCancel(ErrInvalidReportType),
},
{
name: "report base information, database error, retry job",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return(
nil, zerrors.ThrowInternal(nil, "id", "db error"),
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
return mock.NewMockTelemetryServiceClient(gomock.NewController(t))
},
queue: func(t *testing.T) Queue {
return mock.NewMockQueue(gomock.NewController(t))
},
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportType: ReportTypeBaseInformation,
},
},
},
wantErr: zerrors.ThrowInternal(nil, "id", "db error"),
},
{
name: "report base information, config error, cancel job",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return(
&query.Instances{
Instances: []*query.Instance{
{
ID: "id",
CreationDate: testNow,
Domains: []*query.InstanceDomain{
{
Domain: "domain",
},
},
},
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{
SystemId: "system-id",
Version: "version",
Instances: []*analytics.InstanceInformation{
{
Id: "id",
Domains: []string{"domain"},
CreatedAt: timestamppb.New(testNow),
},
},
}).Return(
nil, &TelemetryError{StatusCode: http.StatusNotFound, Body: []byte("endpoint not found")},
)
return client
},
queue: func(t *testing.T) Queue {
return mock.NewMockQueue(gomock.NewController(t))
},
systemID: "system-id",
version: "version",
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportType: ReportTypeBaseInformation,
},
},
},
wantErr: river.JobCancel(&TelemetryError{StatusCode: http.StatusNotFound, Body: []byte("endpoint not found")}),
},
{
name: "report base information, no reports enabled, no error",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return(
&query.Instances{
Instances: []*query.Instance{
{
ID: "id",
CreationDate: testNow,
Domains: []*query.InstanceDomain{
{
Domain: "domain",
},
},
},
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{
SystemId: "system-id",
Version: "version",
Instances: []*analytics.InstanceInformation{
{
Id: "id",
Domains: []string{"domain"},
CreatedAt: timestamppb.New(testNow),
},
},
}).Return(
&analytics.ReportBaseInformationResponse{
ReportId: "report-id",
}, nil,
)
return client
},
queue: func(t *testing.T) Queue {
return mock.NewMockQueue(gomock.NewController(t))
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
Enabled: false,
},
},
},
systemID: "system-id",
version: "version",
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportType: ReportTypeBaseInformation,
},
},
},
},
{
name: "report base information, job creation error, cancel job",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return(
&query.Instances{
Instances: []*query.Instance{
{
ID: "id",
CreationDate: testNow,
Domains: []*query.InstanceDomain{
{
Domain: "domain",
},
},
},
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{
SystemId: "system-id",
Version: "version",
Instances: []*analytics.InstanceInformation{
{
Id: "id",
Domains: []string{"domain"},
CreatedAt: timestamppb.New(testNow),
},
},
}).Return(
&analytics.ReportBaseInformationResponse{
ReportId: "report-id",
}, nil,
)
return client
},
queue: func(t *testing.T) Queue {
q := mock.NewMockQueue(gomock.NewController(t))
q.EXPECT().Insert(gomock.Any(),
&ServicePingReport{
ReportID: "report-id",
ReportType: ReportTypeResourceCounts,
},
gomock.AssignableToTypeOf(reflect.TypeOf(queue.WithQueueName(QueueName))),
gomock.AssignableToTypeOf(reflect.TypeOf(queue.WithMaxAttempts(5)))). // TODO: better solution
Return(errInsert)
return q
},
config: &Config{
MaxAttempts: 5,
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
Enabled: true,
},
},
},
systemID: "system-id",
version: "version",
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportType: ReportTypeBaseInformation,
},
},
},
wantErr: errInsert,
},
{
name: "report base information, success, no error",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return(
&query.Instances{
Instances: []*query.Instance{
{
ID: "id",
CreationDate: testNow,
Domains: []*query.InstanceDomain{
{
Domain: "domain",
},
},
},
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{
SystemId: "system-id",
Version: "version",
Instances: []*analytics.InstanceInformation{
{
Id: "id",
Domains: []string{"domain"},
CreatedAt: timestamppb.New(testNow),
},
},
}).Return(
&analytics.ReportBaseInformationResponse{
ReportId: "report-id",
}, nil,
)
return client
},
queue: func(t *testing.T) Queue {
q := mock.NewMockQueue(gomock.NewController(t))
q.EXPECT().Insert(gomock.Any(),
&ServicePingReport{
ReportID: "report-id",
ReportType: ReportTypeResourceCounts,
},
gomock.AssignableToTypeOf(reflect.TypeOf(queue.WithQueueName(QueueName))),
gomock.AssignableToTypeOf(reflect.TypeOf(queue.WithMaxAttempts(5)))).
Return(nil)
return q
},
config: &Config{
MaxAttempts: 5,
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
Enabled: true,
},
},
},
systemID: "system-id",
version: "version",
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportType: ReportTypeBaseInformation,
},
},
},
},
{
name: "report resource counts, service unavailable, retry job",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return(
[]query.ResourceCount{
{
ID: 1,
InstanceID: "instance-id",
TableName: "table_name",
ParentType: domain.CountParentTypeInstance,
ParentID: "instance-id",
Resource: "resource",
UpdatedAt: testNow,
Amount: 10,
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{
SystemId: "system-id",
ReportId: gu.Ptr("report-id"),
ResourceCounts: []*analytics.ResourceCount{
{
InstanceId: "instance-id",
TableName: "table_name",
ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE,
ParentId: "instance-id",
ResourceName: "resource",
UpdatedAt: timestamppb.New(testNow),
Amount: 10,
},
},
}).Return(
nil, status.Error(codes.Unavailable, "service unavailable"),
)
return client
},
queue: func(t *testing.T) Queue {
return mock.NewMockQueue(gomock.NewController(t))
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
BulkSize: 2,
},
},
},
systemID: "system-id",
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportType: ReportTypeResourceCounts,
ReportID: "report-id",
},
},
},
wantErr: status.Error(codes.Unavailable, "service unavailable"),
},
{
name: "report resource counts, precondition error, cancel job",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return(
[]query.ResourceCount{
{
ID: 1,
InstanceID: "instance-id",
TableName: "table_name",
ParentType: domain.CountParentTypeInstance,
ParentID: "instance-id",
Resource: "resource",
UpdatedAt: testNow,
Amount: 10,
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{
SystemId: "system-id",
ReportId: gu.Ptr("report-id"),
ResourceCounts: []*analytics.ResourceCount{
{
InstanceId: "instance-id",
TableName: "table_name",
ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE,
ParentId: "instance-id",
ResourceName: "resource",
UpdatedAt: timestamppb.New(testNow),
Amount: 10,
},
},
}).Return(
nil, &TelemetryError{StatusCode: http.StatusPreconditionFailed, Body: []byte("report too old")},
)
return client
},
queue: func(t *testing.T) Queue {
return mock.NewMockQueue(gomock.NewController(t))
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
BulkSize: 2,
},
},
},
systemID: "system-id",
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportID: "report-id",
ReportType: ReportTypeResourceCounts,
},
},
},
wantErr: river.JobCancel(&TelemetryError{StatusCode: http.StatusPreconditionFailed, Body: []byte("report too old")}),
},
{
name: "report resource counts, success, no error",
fields: fields{
db: func(t *testing.T) Queries {
queries := mock.NewMockQueries(gomock.NewController(t))
queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return(
[]query.ResourceCount{
{
ID: 1,
InstanceID: "instance-id",
TableName: "table_name",
ParentType: domain.CountParentTypeInstance,
ParentID: "instance-id",
Resource: "resource",
UpdatedAt: testNow,
Amount: 10,
},
}, nil,
)
return queries
},
reportClient: func(t *testing.T) analytics.TelemetryServiceClient {
client := mock.NewMockTelemetryServiceClient(gomock.NewController(t))
client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{
SystemId: "system-id",
ReportId: gu.Ptr("report-id"),
ResourceCounts: []*analytics.ResourceCount{
{
InstanceId: "instance-id",
TableName: "table_name",
ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE,
ParentId: "instance-id",
ResourceName: "resource",
UpdatedAt: timestamppb.New(testNow),
Amount: 10,
},
},
}).Return(
&analytics.ReportResourceCountsResponse{
ReportId: "report-id",
}, nil,
)
return client
},
queue: func(t *testing.T) Queue {
return mock.NewMockQueue(gomock.NewController(t))
},
config: &Config{
Telemetry: TelemetryConfig{
ResourceCount: ResourceCount{
BulkSize: 2,
},
},
},
systemID: "system-id",
},
args: args{
ctx: context.Background(),
job: &river.Job[*ServicePingReport]{
Args: &ServicePingReport{
ReportID: "report-id",
ReportType: ReportTypeResourceCounts,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &Worker{
WorkerDefaults: river.WorkerDefaults[*ServicePingReport]{},
reportClient: tt.fields.reportClient(t),
db: tt.fields.db(t),
queue: tt.fields.queue(t),
config: tt.fields.config,
systemID: tt.fields.systemID,
version: tt.fields.version,
}
err := w.Work(tt.args.ctx, tt.args.job)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func Test_parseAndValidateSchedule(t *testing.T) {
type args struct {
interval string
}
tests := []struct {
name string
args args
wantNextStart time.Time
wantNextEnd time.Time
wantErr error
}{
{
name: "@daily, returns randomized daily schedule",
args: args{
interval: "@daily",
},
wantNextStart: time.Now(),
wantNextEnd: time.Now().Add(24 * time.Hour),
},
{
name: "invalid cron expression, returns error",
args: args{
interval: "invalid cron",
},
wantErr: zerrors.ThrowInvalidArgument(nil, "SERV-NJqiof", "invalid interval"),
},
{
name: "valid cron expression, returns schedule",
args: args{
interval: "0 0 * * *",
},
wantNextStart: nextMidnight(),
wantNextEnd: nextMidnight(),
},
{
name: "valid cron expression (extended syntax), returns schedule",
args: args{
interval: "@midnight",
},
wantNextStart: nextMidnight(),
wantNextEnd: nextMidnight(),
},
{
name: "less than minInterval, returns error",
args: args{
interval: "0/15 * * * *",
},
wantErr: zerrors.ThrowInvalidArgumentf(nil, "SERV-FJ12", "interval must be at least %s", minInterval),
},
{
name: "less than minInterval (extended syntax), returns error",
args: args{
interval: "@every 15m",
},
wantErr: zerrors.ThrowInvalidArgumentf(nil, "SERV-FJ12", "interval must be at least %s", minInterval),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseAndValidateSchedule(tt.args.interval)
assert.ErrorIs(t, err, tt.wantErr)
if tt.wantErr == nil {
now := time.Now()
assert.WithinRange(t, got.Next(now), tt.wantNextStart, tt.wantNextEnd)
}
})
}
}
func nextMidnight() time.Time {
year, month, day := time.Now().Date()
return time.Date(year, month, day+1, 0, 0, 0, 0, time.Local)
}