diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 41da514f5..a68282af3 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -3,6 +3,9 @@ package utils import ( "strconv" "strings" + "time" + + "github.com/newrelic/newrelic-client-go/v2/pkg/nrtime" ) // IntArrayToString converts an array of integers @@ -17,3 +20,21 @@ func IntArrayToString(integers []int) string { return strings.Join(sArray, ",") } + +// a helper function that is used to populate a timestamp with non-zero milliseconds +// if the millisecond count has been found zero, usually when generated with time.Time() +// in Golang which does not have a nanoseconds field; which helps mutations such as those +// in changetracking, the API of which requires a timestamp with non-zero milliseconds. +func GetSafeTimestampWithMilliseconds(inputTimestamp nrtime.EpochMilliseconds) nrtime.EpochMilliseconds { + timestamp := time.Time(inputTimestamp) + + // since time.Time in Go does not have a milliseconds field, which is why the implementation + // of unmarshaling time.Time into a Unix timestamp in the serialization package relies on + // nanoseconds to produce a value of milliseconds, we try employing a similar logic below + + if timestamp.Nanosecond() < 100000000 { + timestamp = timestamp.Add(time.Nanosecond * 100000000) + } + + return nrtime.EpochMilliseconds(timestamp) +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index c3338dd0d..8234ba62c 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -4,8 +4,11 @@ package utils import ( "testing" + "time" + "github.com/newrelic/newrelic-client-go/v2/pkg/nrtime" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIntArrayToString(t *testing.T) { @@ -25,3 +28,77 @@ func TestIntArrayToString(t *testing.T) { result = IntArrayToString([]int{1, 2, 3, 4}) assert.Equal(t, "1,2,3,4", result) } + +func TestGetSafeTimestampWithMilliseconds_ZeroNanoseconds(t *testing.T) { + t.Parallel() + + now := time.Now() + actualNanoseconds := 0 + expectedNanoseconds := actualNanoseconds + 100000000 + + actualResult := GetSafeTimestampWithMilliseconds(nrtime.EpochMilliseconds( + time.Date( + now.Year(), + now.Month(), + now.Day(), + now.Hour()-3, + now.Minute()-30, + 0, + actualNanoseconds, + time.Local, + ), + )) + + expectedResult := nrtime.EpochMilliseconds( + time.Date( + now.Year(), + now.Month(), + now.Day(), + now.Hour()-3, + now.Minute()-30, + 0, + expectedNanoseconds, + time.Local, + ), + ) + require.Equal(t, actualResult, expectedResult) +} + +func TestGetSafeTimestampWithMilliseconds_NonZeroNanoseconds(t *testing.T) { + t.Parallel() + + now := time.Now() + actualNanoseconds := 123000000 + + // equal, since actualNanoSeconds > 100000000 + expectedNanoseconds := actualNanoseconds + + actualResult := GetSafeTimestampWithMilliseconds(nrtime.EpochMilliseconds( + time.Date( + now.Year(), + now.Month(), + now.Day(), + now.Hour()-3, + now.Minute()-30, + 0, + actualNanoseconds, + time.Local, + ), + )) + + expectedResult := nrtime.EpochMilliseconds( + time.Date( + now.Year(), + now.Month(), + now.Day(), + now.Hour()-3, + now.Minute()-30, + 0, + expectedNanoseconds, + time.Local, + ), + ) + + require.Equal(t, actualResult, expectedResult) + +} diff --git a/pkg/changetracking/changetracking_api.go b/pkg/changetracking/changetracking_api.go index 69d3c3e91..f34198688 100644 --- a/pkg/changetracking/changetracking_api.go +++ b/pkg/changetracking/changetracking_api.go @@ -1,13 +1,25 @@ // Code generated by tutone: DO NOT EDIT package changetracking -import "context" +import ( + "context" + + "github.com/newrelic/newrelic-client-go/v2/internal/utils" +) // Creates a new deployment record in NRDB and its associated deployment marker. func (a *Changetracking) ChangeTrackingCreateDeployment( dataHandlingRules ChangeTrackingDataHandlingRules, deployment ChangeTrackingDeploymentInput, ) (*ChangeTrackingDeployment, error) { + // DO NOT DELETE the following function call + // This is NOT covered by Tutone, but is needed to reformat milliseconds in timestamps + // in order to align with the expected format of the timestamp by the API + inputTimestamp := deployment.Timestamp + timestamp := utils.GetSafeTimestampWithMilliseconds(inputTimestamp) + deployment.Timestamp = timestamp + // DO NOT DELETE the above function call + return a.ChangeTrackingCreateDeploymentWithContext(context.Background(), dataHandlingRules, deployment, diff --git a/pkg/changetracking/changetracking_api_integration_test.go b/pkg/changetracking/changetracking_api_integration_test.go index 68b5a57cb..365740dec 100644 --- a/pkg/changetracking/changetracking_api_integration_test.go +++ b/pkg/changetracking/changetracking_api_integration_test.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "log" + "regexp" "testing" "time" @@ -66,7 +67,7 @@ func TestChangeTrackingCreateDeployment_CustomAttributes(t *testing.T) { DeepLink: "newrelic-client-go", DeploymentType: ChangeTrackingDeploymentTypeTypes.BASIC, Description: "This is a test description", - EntityGUID: common.EntityGUID(testhelpers.IntegrationTestApplicationEntityGUID), + EntityGUID: common.EntityGUID(testhelpers.IntegrationTestApplicationEntityGUIDNew), GroupId: "deployment", Timestamp: nrtime.EpochMilliseconds(time.Now()), User: "newrelic-go-client", @@ -109,6 +110,123 @@ func TestChangeTrackingCreateDeployment_TimestampError(t *testing.T) { require.Nil(t, res) } +func TestChangeTrackingCreateDeployment_OlderThan24HoursTimestampError(t *testing.T) { + t.Parallel() + now := time.Now() + + a := newIntegrationTestClient(t) + + input := ChangeTrackingDeploymentInput{ + Changelog: "test", + Commit: "12345a", + DeepLink: "newrelic-client-go", + DeploymentType: ChangeTrackingDeploymentTypeTypes.BASIC, + Description: "This is a test description", + EntityGUID: common.EntityGUID(testhelpers.IntegrationTestApplicationEntityGUID), + GroupId: "deployment", + Timestamp: nrtime.EpochMilliseconds( + time.Date( + now.Year(), + now.Month(), + now.Day()-2, + now.Hour()-3, + now.Minute()-30, + 0, + 0, + time.Local, + ), + ), + User: "newrelic-go-client", + Version: "0.0.1", + } + + res, err := a.ChangeTrackingCreateDeployment( + ChangeTrackingDataHandlingRules{ValidationFlags: []ChangeTrackingValidationFlag{ChangeTrackingValidationFlagTypes.FAIL_ON_FIELD_LENGTH}}, + input, + ) + require.Error(t, err) + require.Regexp(t, regexp.MustCompile("not be more than 24 hours"), err.Error()) + require.Nil(t, res) +} + +func TestChangeTrackingCreateDeployment_TimestampZeroNanosecondsTest(t *testing.T) { + t.Parallel() + + a := newIntegrationTestClient(t) + now := time.Now() + + input := ChangeTrackingDeploymentInput{ + Changelog: "test", + Commit: "12345a", + DeepLink: "newrelic-client-go", + DeploymentType: ChangeTrackingDeploymentTypeTypes.BASIC, + Description: "This is a test description", + EntityGUID: common.EntityGUID(testhelpers.IntegrationTestApplicationEntityGUIDNew), + GroupId: "deployment", + Timestamp: nrtime.EpochMilliseconds( + time.Date( + now.Year(), + now.Month(), + now.Day(), + now.Hour()-3, + now.Minute()-30, + 0, + 0, + time.Local, + ), + ), + User: "newrelic-go-client", + Version: "0.0.1", + } + + res, err := a.ChangeTrackingCreateDeployment( + ChangeTrackingDataHandlingRules{ValidationFlags: []ChangeTrackingValidationFlag{ChangeTrackingValidationFlagTypes.FAIL_ON_FIELD_LENGTH}}, + input, + ) + require.NoError(t, err) + require.NotNil(t, res.EntityGUID) + require.Equal(t, res.EntityGUID, input.EntityGUID) +} + +func TestChangeTrackingCreateDeployment_TimestampNonZeroNanosecondsTest(t *testing.T) { + t.Parallel() + + a := newIntegrationTestClient(t) + now := time.Now() + + input := ChangeTrackingDeploymentInput{ + Changelog: "test", + Commit: "12345a", + DeepLink: "newrelic-client-go", + DeploymentType: ChangeTrackingDeploymentTypeTypes.BASIC, + Description: "This is a test description", + EntityGUID: common.EntityGUID(testhelpers.IntegrationTestApplicationEntityGUIDNew), + GroupId: "deployment", + Timestamp: nrtime.EpochMilliseconds( + time.Date( + now.Year(), + now.Month(), + now.Day(), + now.Hour()-6, + now.Minute()-30, + 0, + 231567, + time.Local, + ), + ), + User: "newrelic-go-client", + Version: "0.0.1", + } + + res, err := a.ChangeTrackingCreateDeployment( + ChangeTrackingDataHandlingRules{ValidationFlags: []ChangeTrackingValidationFlag{ChangeTrackingValidationFlagTypes.FAIL_ON_FIELD_LENGTH}}, + input, + ) + require.NoError(t, err) + require.NotNil(t, res.EntityGUID) + require.Equal(t, res.EntityGUID, input.EntityGUID) +} + func newIntegrationTestClient(t *testing.T) Changetracking { tc := testhelpers.NewIntegrationTestConfig(t)