From 7effd60d2764f57146a3a142993b9cf1161249a8 Mon Sep 17 00:00:00 2001 From: Maximilien Cuony Date: Fri, 11 Sep 2026 17:09:03 +0200 Subject: [PATCH 1/3] [raft] Fix start action missing required context --- cmds/core-service/main.go | 9 +++++++++ pkg/random/random.go | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cmds/core-service/main.go b/cmds/core-service/main.go index 8e337119b..c83b270b1 100644 --- a/cmds/core-service/main.go +++ b/cmds/core-service/main.go @@ -112,6 +112,15 @@ func createAuxServer(ctx context.Context, locality string, publicEndpoint string return nil, stacktrace.Propagate(err, "Unable to interact with store") } + ctx = timestamp.NewContext(ctx, time.Now()) + + seed, err := random.NewSeed() + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to generate seed") + } + + ctx = random.NewContext(ctx, seed) + err = repo.SaveOwnMetadata(ctx, locality, publicEndpoint) if err != nil { diff --git a/pkg/random/random.go b/pkg/random/random.go index 99b20dc81..fa8d9a8c7 100644 --- a/pkg/random/random.go +++ b/pkg/random/random.go @@ -17,7 +17,7 @@ import ( type key struct{} -func newSeed() (int64, error) { +func NewSeed() (int64, error) { var buf [8]byte _, err := rand.Read(buf[:]) if err != nil { @@ -66,7 +66,7 @@ func Generator(seed int64, label string) (*mrand.Rand, error) { // deterministically via Generator. func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - seed, err := newSeed() + seed, err := NewSeed() if err != nil { http.Error(w, "failed to generate request seed", http.StatusInternalServerError) return From 3ec48a13ff3b08101dbae19be51155c2141a23bd Mon Sep 17 00:00:00 2001 From: Mariem Baccari Date: Thu, 24 Sep 2026 15:53:03 +0200 Subject: [PATCH 2/3] [raft/rid] RID API Volume to CellsVolume4D methods --- pkg/rid/models/api/v1/conversions.go | 34 +++++++++ pkg/rid/models/api/v1/conversions_test.go | 70 ++++++++++++++++++ pkg/rid/models/api/v2/conversions.go | 58 +++++++++++++++ pkg/rid/models/api/v2/conversions_test.go | 89 +++++++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 pkg/rid/models/api/v1/conversions_test.go create mode 100644 pkg/rid/models/api/v2/conversions_test.go diff --git a/pkg/rid/models/api/v1/conversions.go b/pkg/rid/models/api/v1/conversions.go index 826a40b48..7cc55aad3 100644 --- a/pkg/rid/models/api/v1/conversions.go +++ b/pkg/rid/models/api/v1/conversions.go @@ -13,6 +13,7 @@ import ( // === RID -> Business === // FromVolume4D converts RID v1 REST model to business object +// TODO: remove once everything is parsed directly into CellsVolume4D func FromVolume4D(vol4 *restapi.Volume4D) (*dssmodels.Volume4D, error) { result := &dssmodels.Volume4D{ SpatialVolume: FromVolume3D(&vol4.SpatialVolume), @@ -37,7 +38,40 @@ func FromVolume4D(vol4 *restapi.Volume4D) (*dssmodels.Volume4D, error) { return result, nil } +// CellsVolume4DFromRest converts RID v1 REST model to CellsVolume4D +func CellsVolume4DFromRest(vol4 *restapi.Volume4D) (*dssmodels.CellsVolume4D, error) { + cells, err := FromGeoPolygon(&vol4.SpatialVolume.Footprint).CalculateCovering() + if err != nil { + return nil, stacktrace.Propagate(err, "Error calculating footprint covering") + } + + result := &dssmodels.CellsVolume4D{ + Cells: cells, + AltitudeLo: (*float32)(vol4.SpatialVolume.AltitudeLo), + AltitudeHi: (*float32)(vol4.SpatialVolume.AltitudeHi), + } + + if vol4.TimeStart != nil { + ts, err := time.Parse(time.RFC3339Nano, *vol4.TimeStart) + if err != nil { + return nil, stacktrace.Propagate(err, "Error converting start time") + } + result.StartTime = &ts + } + + if vol4.TimeEnd != nil { + ts, err := time.Parse(time.RFC3339Nano, *vol4.TimeEnd) + if err != nil { + return nil, stacktrace.Propagate(err, "Error converting end time") + } + result.EndTime = &ts + } + + return result, nil +} + // FromVolume3D converts RID v1 REST model to business object +// TODO: remove along with FromVolume4D. func FromVolume3D(vol3 *restapi.Volume3D) *dssmodels.Volume3D { return &dssmodels.Volume3D{ Footprint: FromGeoPolygon(&vol3.Footprint), diff --git a/pkg/rid/models/api/v1/conversions_test.go b/pkg/rid/models/api/v1/conversions_test.go new file mode 100644 index 000000000..2eb4466cc --- /dev/null +++ b/pkg/rid/models/api/v1/conversions_test.go @@ -0,0 +1,70 @@ +package apiv1 + +import ( + "testing" + "time" + + restapi "github.com/interuss/dss/pkg/api/ridv1" + "github.com/stretchr/testify/require" +) + +func triangleVolume4D() *restapi.Volume4D { + altLo := restapi.Altitude(100) + altHi := restapi.Altitude(200) + timeStart := "2023-01-01T00:00:00Z" + timeEnd := "2023-01-01T01:00:00Z" + return &restapi.Volume4D{ + SpatialVolume: restapi.Volume3D{ + Footprint: restapi.GeoPolygon{ + Vertices: []restapi.LatLngPoint{ + {Lat: 37.427636, Lng: -122.170502}, + {Lat: 37.408799, Lng: -122.064069}, + {Lat: 37.421265, Lng: -122.086504}, + }, + }, + AltitudeLo: &altLo, + AltitudeHi: &altHi, + }, + TimeStart: &timeStart, + TimeEnd: &timeEnd, + } +} + +func TestCellsVolume4DFromRest(t *testing.T) { + vol4 := triangleVolume4D() + + got, err := CellsVolume4DFromRest(vol4) + require.NoError(t, err) + require.NotNil(t, got) + + wantCells, err := FromGeoPolygon(&vol4.SpatialVolume.Footprint).CalculateCovering() + require.NoError(t, err) + require.Equal(t, wantCells, got.Cells) + + require.NotNil(t, got.StartTime) + require.Equal(t, "2023-01-01T00:00:00Z", got.StartTime.Format(time.RFC3339)) + require.NotNil(t, got.EndTime) + require.Equal(t, "2023-01-01T01:00:00Z", got.EndTime.Format(time.RFC3339)) + + require.NotNil(t, got.AltitudeLo) + require.InDelta(t, float32(100), *got.AltitudeLo, 0) + require.NotNil(t, got.AltitudeHi) + require.InDelta(t, float32(200), *got.AltitudeHi, 0) +} + +func TestCellsVolume4DFromRest_InvalidTime(t *testing.T) { + vol4 := triangleVolume4D() + badTime := "not-a-time" + vol4.TimeStart = &badTime + + _, err := CellsVolume4DFromRest(vol4) + require.Error(t, err) +} + +func TestCellsVolume4DFromRest_InvalidFootprint(t *testing.T) { + vol4 := triangleVolume4D() + vol4.SpatialVolume.Footprint.Vertices = vol4.SpatialVolume.Footprint.Vertices[:2] + + _, err := CellsVolume4DFromRest(vol4) + require.Error(t, err) +} diff --git a/pkg/rid/models/api/v2/conversions.go b/pkg/rid/models/api/v2/conversions.go index da93e465f..3092099aa 100644 --- a/pkg/rid/models/api/v2/conversions.go +++ b/pkg/rid/models/api/v2/conversions.go @@ -3,6 +3,7 @@ package apiv2 import ( "time" + "github.com/golang/geo/s2" restapi "github.com/interuss/dss/pkg/api/ridv2" dssmodels "github.com/interuss/dss/pkg/models" ridmodels "github.com/interuss/dss/pkg/rid/models" @@ -46,6 +47,7 @@ func FromAltitude(alt *restapi.Altitude) (*float32, error) { } // FromVolume4D converts RID v2 REST model to business object +// TODO: remove once everything is parsed directly into CellsVolume4D func FromVolume4D(vol4 *restapi.Volume4D) (*dssmodels.Volume4D, error) { vol3, err := FromVolume3D(&vol4.Volume) if err != nil { @@ -69,6 +71,7 @@ func FromVolume4D(vol4 *restapi.Volume4D) (*dssmodels.Volume4D, error) { } // FromVolume3D converts RID v2 REST model to business object +// TODO: remove along with FromVolume4D. func FromVolume3D(vol3 *restapi.Volume3D) (*dssmodels.Volume3D, error) { altitudeLo, err := FromAltitude(vol3.AltitudeLower) if err != nil { @@ -112,6 +115,61 @@ func FromVolume3D(vol3 *restapi.Volume3D) (*dssmodels.Volume3D, error) { return nil, stacktrace.NewError("Neither outline_polygon nor outline_circle were specified in volume") } +// CellsVolume4DFromRest converts RID v2 REST model to CellsVolume4D +func CellsVolume4DFromRest(vol4 *restapi.Volume4D) (*dssmodels.CellsVolume4D, error) { + cells, err := cellsFromVolume3D(&vol4.Volume) + if err != nil { + return nil, stacktrace.Propagate(err, "Error parsing spatial volume of Volume4D") + } + + altitudeLo, err := FromAltitude(vol4.Volume.AltitudeLower) + if err != nil { + return nil, stacktrace.Propagate(err, "Error parsing lower altitude of Volume3D") + } + altitudeHi, err := FromAltitude(vol4.Volume.AltitudeUpper) + if err != nil { + return nil, stacktrace.Propagate(err, "Error parsing upper altitude of Volume3D") + } + + result := &dssmodels.CellsVolume4D{ + Cells: cells, + AltitudeLo: altitudeLo, + AltitudeHi: altitudeHi, + } + + result.StartTime, err = FromTime(vol4.TimeStart) + if err != nil { + return nil, stacktrace.Propagate(err, "Error parsing start time of Volume4D") + } + result.EndTime, err = FromTime(vol4.TimeEnd) + if err != nil { + return nil, stacktrace.Propagate(err, "Error parsing end time of Volume4D") + } + + return result, nil +} + +// cellsFromVolume3D computes the spatial covering of vol3's outline directly, without +// constructing an intermediate GeoCircle/GeoPolygon. +func cellsFromVolume3D(vol3 *restapi.Volume3D) (s2.CellUnion, error) { + if vol3.OutlinePolygon != nil { + if vol3.OutlineCircle != nil { + return nil, stacktrace.NewError("Only one of outline_circle or outline_polygon may be specified") + } + return FromPolygon(vol3.OutlinePolygon).CalculateCovering() + } + + if vol3.OutlineCircle != nil { + circle, err := FromCircle(vol3.OutlineCircle) + if err != nil { + return nil, stacktrace.Propagate(err, "Error parsing outline_circle for Volume3D") + } + return circle.CalculateCovering() + } + + return nil, stacktrace.NewError("Neither outline_polygon nor outline_circle were specified in volume") +} + // FromPolygon converts RID v2 REST model to business object func FromPolygon(polygon *restapi.Polygon) *dssmodels.GeoPolygon { result := &dssmodels.GeoPolygon{} diff --git a/pkg/rid/models/api/v2/conversions_test.go b/pkg/rid/models/api/v2/conversions_test.go new file mode 100644 index 000000000..1f72bdecb --- /dev/null +++ b/pkg/rid/models/api/v2/conversions_test.go @@ -0,0 +1,89 @@ +package apiv2 + +import ( + "testing" + "time" + + restapi "github.com/interuss/dss/pkg/api/ridv2" + "github.com/stretchr/testify/require" +) + +func triangleVolume4D() *restapi.Volume4D { + return &restapi.Volume4D{ + Volume: restapi.Volume3D{ + OutlinePolygon: &restapi.Polygon{ + Vertices: []restapi.LatLngPoint{ + {Lat: 37.427636, Lng: -122.170502}, + {Lat: 37.408799, Lng: -122.064069}, + {Lat: 37.421265, Lng: -122.086504}, + }, + }, + AltitudeLower: &restapi.Altitude{Value: 100, Reference: "W84", Units: "M"}, + AltitudeUpper: &restapi.Altitude{Value: 200, Reference: "W84", Units: "M"}, + }, + TimeStart: &restapi.Time{Value: "2023-01-01T00:00:00Z", Format: "RFC3339"}, + TimeEnd: &restapi.Time{Value: "2023-01-01T01:00:00Z", Format: "RFC3339"}, + } +} + +func circleVolume4D() *restapi.Volume4D { + return &restapi.Volume4D{ + Volume: restapi.Volume3D{ + OutlineCircle: &restapi.Circle{ + Center: &restapi.LatLngPoint{Lat: 37.427636, Lng: -122.170502}, + Radius: &restapi.Radius{Value: 300, Units: "M"}, + }, + }, + } +} + +func TestCellsVolume4DFromRest_Polygon(t *testing.T) { + vol4 := triangleVolume4D() + + got, err := CellsVolume4DFromRest(vol4) + require.NoError(t, err) + require.NotNil(t, got) + + wantCells, err := FromPolygon(vol4.Volume.OutlinePolygon).CalculateCovering() + require.NoError(t, err) + require.Equal(t, wantCells, got.Cells) + + require.NotNil(t, got.StartTime) + require.Equal(t, "2023-01-01T00:00:00Z", got.StartTime.Format(time.RFC3339)) + require.NotNil(t, got.EndTime) + require.Equal(t, "2023-01-01T01:00:00Z", got.EndTime.Format(time.RFC3339)) + + require.NotNil(t, got.AltitudeLo) + require.InDelta(t, float32(100), *got.AltitudeLo, 0) + require.NotNil(t, got.AltitudeHi) + require.InDelta(t, float32(200), *got.AltitudeHi, 0) +} + +func TestCellsVolume4DFromRest_Circle(t *testing.T) { + vol4 := circleVolume4D() + + got, err := CellsVolume4DFromRest(vol4) + require.NoError(t, err) + require.NotNil(t, got) + + circle, err := FromCircle(vol4.Volume.OutlineCircle) + require.NoError(t, err) + wantCells, err := circle.CalculateCovering() + require.NoError(t, err) + require.Equal(t, wantCells, got.Cells) +} + +func TestCellsVolume4DFromRest_BothOutlinesSpecified(t *testing.T) { + vol4 := triangleVolume4D() + vol4.Volume.OutlineCircle = circleVolume4D().Volume.OutlineCircle + + _, err := CellsVolume4DFromRest(vol4) + require.Error(t, err) +} + +func TestCellsVolume4DFromRest_NoOutlineSpecified(t *testing.T) { + vol4 := &restapi.Volume4D{} + + _, err := CellsVolume4DFromRest(vol4) + require.Error(t, err) +} From 282cb73082f09b410e005178f7a635ee6476883f Mon Sep 17 00:00:00 2001 From: Mariem Baccari Date: Mon, 31 Aug 2026 10:44:21 +0200 Subject: [PATCH 3/3] [raft/rid] Extract InsertSubscription and route Create through the store --- pkg/rid/application/isa_test.go | 41 ++- pkg/rid/application/subscription.go | 43 --- pkg/rid/application/subscription_test.go | 193 +++--------- pkg/rid/models/subscriptions.go | 6 +- pkg/rid/operations/subscription.go | 84 +++++ pkg/rid/operations/subscription_test.go | 312 +++++++++++++++++++ pkg/rid/server/v1/server_test.go | 96 ++---- pkg/rid/server/v1/subscription_handler.go | 34 +- pkg/rid/server/v2/subscription_handler.go | 34 +- pkg/rid/store/memstore/store_test.go | 6 +- pkg/rid/store/memstore/subscriptions.go | 12 +- pkg/rid/store/memstore/subscriptions_test.go | 48 +-- pkg/rid/store/sqlstore/store_test.go | 6 +- pkg/rid/store/sqlstore/subscriptions.go | 2 +- pkg/rid/store/sqlstore/subscriptions_test.go | 48 +-- 15 files changed, 594 insertions(+), 371 deletions(-) create mode 100644 pkg/rid/operations/subscription_test.go diff --git a/pkg/rid/application/isa_test.go b/pkg/rid/application/isa_test.go index f620d302b..2e650c564 100644 --- a/pkg/rid/application/isa_test.go +++ b/pkg/rid/application/isa_test.go @@ -11,6 +11,7 @@ import ( dsserr "github.com/interuss/dss/pkg/errors" dssmodels "github.com/interuss/dss/pkg/models" ridmodels "github.com/interuss/dss/pkg/rid/models" + "github.com/interuss/dss/pkg/timestamp" "github.com/interuss/stacktrace" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -95,7 +96,7 @@ func (store *isaStore) CountISAs(ctx context.Context) (int64, error) { } func TestISAUpdateIdxCells(t *testing.T) { - ctx := context.Background() + ctx := timestamp.NewContext(t.Context(), fakeClock.Now()) app, cleanup := setUpISAApp(ctx, t) defer cleanup() @@ -119,21 +120,28 @@ func TestISAUpdateIdxCells(t *testing.T) { // with the soon to be new version of the isa. both should increase their // notification index. - _, err = app.InsertSubscription(ctx, &ridmodels.Subscription{ - ID: dssmodels.ID(uuid.New().String()), - Owner: "owner", - StartTime: &startTime, - EndTime: &endTime, - Cells: s2.CellUnion{17106221850767130624, 17106221919486607360}, + repo, err := app.store.Interact(ctx) + require.NoError(t, err) + + _, err = repo.InsertSubscription(ctx, &ridmodels.Subscription{ + ID: dssmodels.ID(uuid.New().String()), + Owner: "owner", + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + Cells: s2.CellUnion{17106221850767130624, 17106221919486607360}, + }, }) require.NoError(t, err) - _, err = app.InsertSubscription(ctx, &ridmodels.Subscription{ - ID: dssmodels.ID(uuid.New().String()), - Owner: "owner", - StartTime: &startTime, - EndTime: &endTime, - Cells: s2.CellUnion{17106221953846345728}, + _, err = repo.InsertSubscription(ctx, &ridmodels.Subscription{ + ID: dssmodels.ID(uuid.New().String()), + Owner: "owner", + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + Cells: s2.CellUnion{17106221953846345728}, + }, }) require.NoError(t, err) @@ -326,7 +334,7 @@ func TestUpdateISA(t *testing.T) { func TestAppDeleteISAs(t *testing.T) { var ( - ctx = context.Background() + ctx = timestamp.NewContext(t.Context(), fakeClock.Now()) app, cleanup = setUpISAApp(ctx, t) ) defer cleanup() @@ -334,7 +342,10 @@ func TestAppDeleteISAs(t *testing.T) { insertedSubscriptions := []*ridmodels.Subscription{} for _, r := range subscriptionsPool { sunscriptionCopy := *r.input - s1, err := app.InsertSubscription(ctx, &sunscriptionCopy) + repo, err := app.store.Interact(ctx) + require.NoError(t, err) + + s1, err := repo.InsertSubscription(ctx, &sunscriptionCopy) require.NoError(t, err) require.NotNil(t, s1) require.Equal(t, 42, s1.NotificationIndex) diff --git a/pkg/rid/application/subscription.go b/pkg/rid/application/subscription.go index 706d58616..d13f6296c 100644 --- a/pkg/rid/application/subscription.go +++ b/pkg/rid/application/subscription.go @@ -25,9 +25,6 @@ const ( type SubscriptionApp interface { GetSubscription(ctx context.Context, id dssmodels.ID) (*ridmodels.Subscription, error) - // InsertSubscription inserts or updates an Subscription. - InsertSubscription(ctx context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) - // UpdateSubscription UpdateSubscription(ctx context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) @@ -51,46 +48,6 @@ func (a *app) SearchSubscriptionsByOwner(ctx context.Context, cells s2.CellUnion return repo.SearchSubscriptionsByOwner(ctx, cells, owner) } -func (a *app) InsertSubscription(ctx context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) { - // Validate and perhaps correct StartTime and EndTime. - if err := s.AdjustTimeRange(a.clock.Now(), nil); err != nil { - return nil, stacktrace.Propagate(err, "Unable to adjust time range") - } - var sub *ridmodels.Subscription - _, err := a.store.Transact(ctx, store.NewFuncOperation(func(ctx context.Context, repo repos.Repository) error { - - // ensure it doesn't exist yet - old, err := repo.GetSubscription(ctx, s.ID) - if err != nil { - return stacktrace.Propagate(err, "Error getting Subscription from repo") - } - if old != nil { - return stacktrace.NewErrorWithCode(dsserr.AlreadyExists, "Subscription %s already exists", s.ID) - } - - // Check the user hasn't created too many subscriptions in this area. - count, err := repo.MaxSubscriptionCountInCellsByOwner(ctx, s.Cells, s.Owner) - if err != nil { - a.logger.Error("Error fetching max subscription count", zap.Error(err)) - return stacktrace.Propagate(err, - "Failed to fetch subscription count, rejecting request") - } - if count >= maxSubscriptionsPerArea { - return stacktrace.Propagate( - stacktrace.NewErrorWithCode(dsserr.Exhausted, "Too many existing subscriptions in this area already"), - "%s had %d subscriptions in the area", s.Owner, count) - } - - sub, err = repo.InsertSubscription(ctx, s) - if err != nil { - return stacktrace.Propagate(err, "Error inserting Subscription into repo") - } - - return nil - })) - return sub, err -} - // InsertSubscription implements the App InsertSubscription method func (a *app) UpdateSubscription(ctx context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) { var sub *ridmodels.Subscription diff --git a/pkg/rid/application/subscription_test.go b/pkg/rid/application/subscription_test.go index 32317d5fe..e6712ab02 100644 --- a/pkg/rid/application/subscription_test.go +++ b/pkg/rid/application/subscription_test.go @@ -28,11 +28,13 @@ var ( ID: dssmodels.ID(uuid.New().String()), Owner: dssmodels.Owner(uuid.New().String()), URL: "https://no/place/like/home", - StartTime: &startTime, - EndTime: &endTime, NotificationIndex: 42, - Cells: s2.CellUnion{ - 12494535935418957824, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + Cells: s2.CellUnion{ + 12494535935418957824, + }, }, }, }, @@ -42,10 +44,12 @@ var ( ID: dssmodels.ID(uuid.New().String()), Owner: dssmodels.Owner(uuid.New().String()), URL: "https://no/place/like/home", - EndTime: &endTime, NotificationIndex: 42, - Cells: s2.CellUnion{ - 12494535935418957824, + CellsVolume4D: &dssmodels.CellsVolume4D{ + EndTime: &endTime, + Cells: s2.CellUnion{ + 12494535935418957824, + }, }, }, }, @@ -172,13 +176,16 @@ func TestBadOwner(t *testing.T) { app, cleanup := setUpSubApp(ctx, t) defer cleanup() - sub := &ridmodels.Subscription{ + repo, err := app.store.Interact(ctx) + require.NoError(t, err) + + sub, err := repo.InsertSubscription(ctx, &ridmodels.Subscription{ ID: dssmodels.ID(uuid.New().String()), Owner: "orig Owner", - Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, - } - - sub, err := app.InsertSubscription(ctx, sub) + CellsVolume4D: &dssmodels.CellsVolume4D{ + Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, + }, + }) require.NoError(t, err) // Test changing owner fails sub.Owner = "new bad owner" @@ -199,12 +206,17 @@ func TestSubscriptionUpdateCells(t *testing.T) { // library might try to Normalize (this is the name of the function) the Union // into a single cell. We don't support this currently, so let's make sure // this doesn't happen. - sub, err := app.InsertSubscription(ctx, &ridmodels.Subscription{ - ID: dssmodels.ID(uuid.New().String()), - Owner: owner, - StartTime: &startTime, - EndTime: &endTime, - Cells: s2.CellUnion{17106221850767130624, 17106221885126868992, 17106221919486607360}, + repo, err := app.store.Interact(ctx) + require.NoError(t, err) + + sub, err := repo.InsertSubscription(ctx, &ridmodels.Subscription{ + ID: dssmodels.ID(uuid.New().String()), + Owner: owner, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + Cells: s2.CellUnion{17106221850767130624, 17106221885126868992, 17106221919486607360}, + }, }) require.NoError(t, err) @@ -222,88 +234,6 @@ func TestSubscriptionUpdateCells(t *testing.T) { require.Len(t, subs, 1) } -func TestInsertSubscriptionsWithTimes(t *testing.T) { - ctx := context.Background() - app, cleanup := setUpSubApp(ctx, t) - defer cleanup() - - for _, r := range []struct { - name string - updateFromStartTime time.Time - updateFromEndTime time.Time - startTime time.Time - endTime time.Time - wantErr stacktrace.ErrorCode - wantStartTime time.Time - wantEndTime time.Time - }{ - { - name: "start-time-defaults-to-now", - endTime: fakeClock.Now().Add(time.Hour), - wantStartTime: fakeClock.Now(), - wantEndTime: fakeClock.Now().Add(time.Hour), - }, - { - name: "end-time-defaults-to-24h", - wantStartTime: fakeClock.Now(), - wantEndTime: fakeClock.Now().Add(24 * time.Hour), - }, - { - name: "start-time-in-the-past", - startTime: fakeClock.Now().Add(-6 * time.Minute), - endTime: fakeClock.Now().Add(time.Hour), - wantErr: dsserr.BadRequest, - }, - { - name: "start-time-slightly-in-the-past", - startTime: fakeClock.Now().Add(-4 * time.Minute), - endTime: fakeClock.Now().Add(time.Hour), - wantStartTime: fakeClock.Now().Add(-4 * time.Minute), - }, - { - name: "end-time-before-start-time", - startTime: fakeClock.Now().Add(20 * time.Minute), - endTime: fakeClock.Now().Add(10 * time.Minute), - wantErr: dsserr.BadRequest, - }, - } { - t.Run(r.name, func(t *testing.T) { - id := dssmodels.ID(uuid.New().String()) - owner := dssmodels.Owner(uuid.New().String()) - var version *dssmodels.Version - - s := &ridmodels.Subscription{ - ID: id, - Owner: owner, - Version: version, - Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, - } - if !r.startTime.IsZero() { - s.StartTime = &r.startTime - } - if !r.endTime.IsZero() { - s.EndTime = &r.endTime - } - sub, err := app.InsertSubscription(ctx, s) - - if r.wantErr == stacktrace.ErrorCode(0) { - require.NoError(t, err) - } else { - require.Equal(t, r.wantErr, stacktrace.GetCode(err)) - } - - if !r.wantStartTime.IsZero() { - require.NotNil(t, sub.StartTime) - require.Equal(t, r.wantStartTime.UTC().Truncate(time.Microsecond), (*sub.StartTime).UTC().Truncate(time.Microsecond)) - } - if !r.wantEndTime.IsZero() { - require.NotNil(t, sub.EndTime) - require.Equal(t, r.wantEndTime.UTC().Truncate(time.Microsecond), (*sub.EndTime).UTC().Truncate(time.Microsecond)) - } - }) - } -} - func TestUpdateSubscriptionsWithTimes(t *testing.T) { ctx := context.Background() app, cleanup := setUpSubApp(ctx, t) @@ -369,11 +299,13 @@ func TestUpdateSubscriptionsWithTimes(t *testing.T) { // Insert a pre-existing subscription to simulate updating from something. existing, err := repo.InsertSubscription(ctx, &ridmodels.Subscription{ - ID: id, - Owner: owner, - StartTime: &r.updateFromStartTime, - EndTime: &r.updateFromEndTime, - Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, + ID: id, + Owner: owner, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &r.updateFromStartTime, + EndTime: &r.updateFromEndTime, + Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, + }, }) require.NoError(t, err) version = existing.Version @@ -382,7 +314,9 @@ func TestUpdateSubscriptionsWithTimes(t *testing.T) { ID: id, Owner: owner, Version: version, - Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, + CellsVolume4D: &dssmodels.CellsVolume4D{ + Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, + }, } if !r.startTime.IsZero() { s.StartTime = &r.startTime @@ -409,50 +343,3 @@ func TestUpdateSubscriptionsWithTimes(t *testing.T) { }) } } - -func TestInsertTooManySubscription(t *testing.T) { - var ( - ctx = context.Background() - app, cleanup = setUpSubApp(ctx, t) - ) - defer cleanup() - // Helper function that makes a subscription with a random ID, fixed owner, - // and provided cellIDs. - makeSubscription := func(cellIDs []uint64) *ridmodels.Subscription { - s := &ridmodels.Subscription{ - ID: dssmodels.ID(uuid.New().String()), - Owner: dssmodels.Owner("bob"), - StartTime: &startTime, - EndTime: &endTime, - Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, - } - - s.Cells = make(s2.CellUnion, len(cellIDs)) - for i, id := range cellIDs { - s.Cells[i] = s2.CellID(id) - } - return s - } - - // We should be able to insert 10 subscriptions without error. - for i := 0; i < 10; i++ { - ret, err := app.InsertSubscription(ctx, makeSubscription([]uint64{12494535901059219456, 12494535866699481088})) - require.NoError(t, err) - require.NotNil(t, &ret) - } - - // Inserting the 11th subscription will fail. - ret, err := app.InsertSubscription(ctx, makeSubscription([]uint64{12494535901059219456, 12494535866699481088})) - require.Equal(t, dsserr.Exhausted, stacktrace.GetCode(err)) - require.Nil(t, ret) - - // Inserting a subscription in a different cell will succeed. - ret, err = app.InsertSubscription(ctx, makeSubscription([]uint64{12494535832339742720})) - require.NoError(t, err) - require.NotNil(t, &ret) - - // Inserting a subscription that overlaps fail. - ret, err = app.InsertSubscription(ctx, makeSubscription([]uint64{12494535935418957824, 12494535866699481088})) - require.Equal(t, dsserr.Exhausted, stacktrace.GetCode(err)) - require.Nil(t, ret) -} diff --git a/pkg/rid/models/subscriptions.go b/pkg/rid/models/subscriptions.go index 8fb17cc9b..1f96c041b 100644 --- a/pkg/rid/models/subscriptions.go +++ b/pkg/rid/models/subscriptions.go @@ -26,13 +26,9 @@ type Subscription struct { URL string NotificationIndex int Owner dssmodels.Owner - Cells s2.CellUnion - StartTime *time.Time - EndTime *time.Time Version *dssmodels.Version - AltitudeHi *float32 - AltitudeLo *float32 Writer string + *dssmodels.CellsVolume4D } // SetCells is a convenience function that accepts an int64 array and converts diff --git a/pkg/rid/operations/subscription.go b/pkg/rid/operations/subscription.go index dee5fac7e..3a9e91e0b 100644 --- a/pkg/rid/operations/subscription.go +++ b/pkg/rid/operations/subscription.go @@ -6,12 +6,40 @@ import ( ridv1 "github.com/interuss/dss/pkg/api/ridv1" ridv2 "github.com/interuss/dss/pkg/api/ridv2" dsserr "github.com/interuss/dss/pkg/errors" + "github.com/interuss/dss/pkg/locality" dssmodels "github.com/interuss/dss/pkg/models" + ridmodels "github.com/interuss/dss/pkg/rid/models" "github.com/interuss/dss/pkg/rid/repos" dssstore "github.com/interuss/dss/pkg/store" + "github.com/interuss/dss/pkg/timestamp" "github.com/interuss/stacktrace" ) +// Defined in requirement DSS0030. +const maxSubscriptionsPerArea = 10 + +type insertSubscriptionPayload struct { + ID dssmodels.ID + Owner dssmodels.Owner + URL string + Version *dssmodels.Version + Extents *dssmodels.CellsVolume4D +} + +func (p *insertSubscriptionPayload) OperationID() string { return ridv2.CreateSubscriptionOperationID } + +// NewInsertSubscriptionPayload performs the request validation that can be done ahead of the +// transaction for a Subscription creation request. +func NewInsertSubscriptionPayload(id dssmodels.ID, owner dssmodels.Owner, url string, version *dssmodels.Version, extents *dssmodels.CellsVolume4D) dssstore.OperationRequest { + return &insertSubscriptionPayload{ + ID: id, + Owner: owner, + URL: url, + Version: version, + Extents: extents, + } +} + func init() { Registry[ridv1.DeleteSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, @@ -23,6 +51,16 @@ func init() { Decode: dssstore.DecodeJSON[*ridv2.DeleteSubscriptionRequest], Execute: executeDeleteSubscription, } + Registry[ridv1.CreateSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ + Encode: dssstore.EncodeJSON, + Decode: dssstore.DecodeJSON[*insertSubscriptionPayload], + Execute: executeInsertSubscription, + } + Registry[ridv2.CreateSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ + Encode: dssstore.EncodeJSON, + Decode: dssstore.DecodeJSON[*insertSubscriptionPayload], + Execute: executeInsertSubscription, + } } func executeDeleteSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { @@ -73,3 +111,49 @@ func executeDeleteSubscription(ctx context.Context, repo repos.Repository, reque } return ret, nil } + +func executeInsertSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { + payload, ok := request.(*insertSubscriptionPayload) + if !ok { + return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, ridv2.CreateSubscriptionOperationID) + } + + sub := &ridmodels.Subscription{ + ID: payload.ID, + Owner: payload.Owner, + URL: payload.URL, + CellsVolume4D: payload.Extents, + Writer: locality.MustFromContext(ctx), + } + + // Validate and perhaps correct StartTime and EndTime. + if err := sub.AdjustTimeRange(timestamp.MustFromContext(ctx), nil); err != nil { + return nil, stacktrace.Propagate(err, "Unable to adjust time range") + } + + // ensure it doesn't exist yet + old, err := repo.GetSubscription(ctx, sub.ID) + if err != nil { + return nil, stacktrace.Propagate(err, "Error getting Subscription from repo") + } + if old != nil { + return nil, stacktrace.NewErrorWithCode(dsserr.AlreadyExists, "Subscription %s already exists", sub.ID) + } + + // Check the user hasn't created too many subscriptions in this area. + count, err := repo.MaxSubscriptionCountInCellsByOwner(ctx, sub.Cells, sub.Owner) + if err != nil { + return nil, stacktrace.Propagate(err, "Failed to fetch subscription count, rejecting request") + } + if count >= maxSubscriptionsPerArea { + return nil, stacktrace.Propagate( + stacktrace.NewErrorWithCode(dsserr.Exhausted, "Too many existing subscriptions in this area already"), + "%s had %d subscriptions in the area", sub.Owner, count) + } + + ret, err := repo.InsertSubscription(ctx, sub) + if err != nil { + return nil, stacktrace.Propagate(err, "Error inserting Subscription into repo") + } + return ret, nil +} diff --git a/pkg/rid/operations/subscription_test.go b/pkg/rid/operations/subscription_test.go new file mode 100644 index 000000000..891f3a3f4 --- /dev/null +++ b/pkg/rid/operations/subscription_test.go @@ -0,0 +1,312 @@ +package operations + +import ( + "context" + "testing" + "time" + + "github.com/golang/geo/s2" + "github.com/google/uuid" + dsserr "github.com/interuss/dss/pkg/errors" + "github.com/interuss/dss/pkg/locality" + dssmodels "github.com/interuss/dss/pkg/models" + ridmodels "github.com/interuss/dss/pkg/rid/models" + "github.com/interuss/dss/pkg/rid/repos" + "github.com/interuss/dss/pkg/timestamp" + "github.com/interuss/stacktrace" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" +) + +var ( + fakeClock = clockwork.NewFakeClock() + startTime = fakeClock.Now().Add(-time.Minute) + endTime = fakeClock.Now().Add(time.Hour) +) + +func newTestContext() context.Context { + ctx := timestamp.NewContext(context.Background(), fakeClock.Now()) + return locality.NewContext(ctx, "test-locality") +} + +func insertSubscription(ctx context.Context, repo repos.Repository, s *ridmodels.Subscription) (*ridmodels.Subscription, error) { + req := NewInsertSubscriptionPayload(s.ID, s.Owner, s.URL, s.Version, &dssmodels.CellsVolume4D{ + Cells: s.Cells, + StartTime: s.StartTime, + EndTime: s.EndTime, + AltitudeLo: s.AltitudeLo, + AltitudeHi: s.AltitudeHi, + }) + ret, err := executeInsertSubscription(ctx, repo, req) + if err != nil { + return nil, err + } + if ret == nil { + return nil, nil + } + return ret.(*ridmodels.Subscription), nil +} + +type fakeSubscriptionRepo struct { + subs map[dssmodels.ID]*ridmodels.Subscription +} + +func newFakeSubscriptionRepo() *fakeSubscriptionRepo { + return &fakeSubscriptionRepo{subs: make(map[dssmodels.ID]*ridmodels.Subscription)} +} + +func (r *fakeSubscriptionRepo) GetSubscription(_ context.Context, id dssmodels.ID) (*ridmodels.Subscription, error) { + if sub, ok := r.subs[id]; ok { + return sub, nil + } + return nil, nil +} + +func (r *fakeSubscriptionRepo) DeleteSubscription(_ context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) { + if sub, ok := r.subs[s.ID]; ok { + delete(r.subs, s.ID) + return sub, nil + } + return nil, nil +} + +func (r *fakeSubscriptionRepo) InsertSubscription(_ context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) { + storedCopy := *s + storedCopy.Version = dssmodels.VersionFromTime(time.Now()) + r.subs[s.ID] = &storedCopy + returnedCopy := storedCopy + return &returnedCopy, nil +} + +func (r *fakeSubscriptionRepo) UpdateSubscription(_ context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) { + storedCopy := *s + storedCopy.Version = dssmodels.VersionFromTime(time.Now()) + r.subs[s.ID] = &storedCopy + returnedCopy := storedCopy + return &returnedCopy, nil +} + +func (r *fakeSubscriptionRepo) SearchSubscriptions(_ context.Context, cells s2.CellUnion) ([]*ridmodels.Subscription, error) { + var subs []*ridmodels.Subscription + for _, s := range r.subs { + appended := false + for _, c1 := range s.Cells { + for _, c2 := range cells { + if c1 == c2 { + subs = append(subs, s) + appended = true + break + } + } + if appended { + break + } + } + } + return subs, nil +} + +func (r *fakeSubscriptionRepo) SearchSubscriptionsByOwner(ctx context.Context, cells s2.CellUnion, owner dssmodels.Owner) ([]*ridmodels.Subscription, error) { + var subs []*ridmodels.Subscription + res, err := r.SearchSubscriptions(ctx, cells) + if err != nil { + return nil, err + } + for _, s := range res { + if s.Owner == owner { + subs = append(subs, s) + } + } + return subs, nil +} + +func (r *fakeSubscriptionRepo) UpdateNotificationIdxsInCells(ctx context.Context, cells s2.CellUnion) ([]*ridmodels.Subscription, error) { + subs, err := r.SearchSubscriptions(ctx, cells) + if err != nil { + return nil, err + } + for i := range subs { + subs[i].NotificationIndex++ + } + return subs, nil +} + +func (r *fakeSubscriptionRepo) MaxSubscriptionCountInCellsByOwner(ctx context.Context, cells s2.CellUnion, owner dssmodels.Owner) (int, error) { + maxValue := 0 + subs, err := r.SearchSubscriptionsByOwner(ctx, cells, owner) + if err != nil { + return 0, err + } + + cellMap := make(map[s2.CellID]int) + for _, s := range subs { + for _, cid := range s.Cells { + cellMap[cid]++ + if cellMap[cid] > maxValue { + maxValue = cellMap[cid] + } + } + } + return maxValue, nil +} + +func (r *fakeSubscriptionRepo) ListExpiredSubscriptions(_ context.Context, _ string, _ time.Time) ([]*ridmodels.Subscription, error) { + return nil, nil +} + +func (r *fakeSubscriptionRepo) CountSubscriptions(_ context.Context) (int64, error) { + return int64(len(r.subs)), nil +} + +func (r *fakeSubscriptionRepo) GetISA(_ context.Context, _ dssmodels.ID, _ bool) (*ridmodels.IdentificationServiceArea, error) { + panic("not implemented") +} + +func (r *fakeSubscriptionRepo) DeleteISA(_ context.Context, _ *ridmodels.IdentificationServiceArea) (*ridmodels.IdentificationServiceArea, error) { + panic("not implemented") +} + +func (r *fakeSubscriptionRepo) InsertISA(_ context.Context, _ *ridmodels.IdentificationServiceArea) (*ridmodels.IdentificationServiceArea, error) { + panic("not implemented") +} + +func (r *fakeSubscriptionRepo) UpdateISA(_ context.Context, _ *ridmodels.IdentificationServiceArea) (*ridmodels.IdentificationServiceArea, error) { + panic("not implemented") +} + +func (r *fakeSubscriptionRepo) SearchISAs(_ context.Context, _ s2.CellUnion, _ *time.Time, _ *time.Time) ([]*ridmodels.IdentificationServiceArea, error) { + panic("not implemented") +} + +func (r *fakeSubscriptionRepo) ListExpiredISAs(_ context.Context, _ string, _ time.Time) ([]*ridmodels.IdentificationServiceArea, error) { + panic("not implemented") +} + +func (r *fakeSubscriptionRepo) CountISAs(_ context.Context) (int64, error) { + panic("not implemented") +} + +func TestInsertSubscriptionsWithTimes(t *testing.T) { + repo := newFakeSubscriptionRepo() + + for _, r := range []struct { + name string + startTime time.Time + endTime time.Time + wantErr stacktrace.ErrorCode + wantStartTime time.Time + wantEndTime time.Time + }{ + { + name: "start-time-defaults-to-now", + endTime: fakeClock.Now().Add(time.Hour), + wantStartTime: fakeClock.Now(), + wantEndTime: fakeClock.Now().Add(time.Hour), + }, + { + name: "end-time-defaults-to-24h", + wantStartTime: fakeClock.Now(), + wantEndTime: fakeClock.Now().Add(24 * time.Hour), + }, + { + name: "start-time-in-the-past", + startTime: fakeClock.Now().Add(-6 * time.Minute), + endTime: fakeClock.Now().Add(time.Hour), + wantErr: dsserr.BadRequest, + }, + { + name: "start-time-slightly-in-the-past", + startTime: fakeClock.Now().Add(-4 * time.Minute), + endTime: fakeClock.Now().Add(time.Hour), + wantStartTime: fakeClock.Now().Add(-4 * time.Minute), + }, + { + name: "end-time-before-start-time", + startTime: fakeClock.Now().Add(20 * time.Minute), + endTime: fakeClock.Now().Add(10 * time.Minute), + wantErr: dsserr.BadRequest, + }, + } { + t.Run(r.name, func(t *testing.T) { + ctx := newTestContext() + id := dssmodels.ID(uuid.New().String()) + owner := dssmodels.Owner(uuid.New().String()) + + s := &ridmodels.Subscription{ + ID: id, + Owner: owner, + CellsVolume4D: &dssmodels.CellsVolume4D{ + Cells: s2.CellUnion{s2.CellID(17106221850767130624)}, + }, + } + if !r.startTime.IsZero() { + s.StartTime = &r.startTime + } + if !r.endTime.IsZero() { + s.EndTime = &r.endTime + } + sub, err := insertSubscription(ctx, repo, s) + + if r.wantErr == stacktrace.ErrorCode(0) { + require.NoError(t, err) + } else { + require.Equal(t, r.wantErr, stacktrace.GetCode(err)) + } + + if !r.wantStartTime.IsZero() { + require.NotNil(t, sub.StartTime) + require.Equal(t, r.wantStartTime.UTC().Truncate(time.Microsecond), (*sub.StartTime).UTC().Truncate(time.Microsecond)) + } + if !r.wantEndTime.IsZero() { + require.NotNil(t, sub.EndTime) + require.Equal(t, r.wantEndTime.UTC().Truncate(time.Microsecond), (*sub.EndTime).UTC().Truncate(time.Microsecond)) + } + }) + } +} + +func TestInsertTooManySubscription(t *testing.T) { + ctx := newTestContext() + repo := newFakeSubscriptionRepo() + + // Helper function that makes a subscription with a random ID, fixed owner, + // and provided cellIDs. + makeSubscription := func(cellIDs []uint64) *ridmodels.Subscription { + s := &ridmodels.Subscription{ + ID: dssmodels.ID(uuid.New().String()), + Owner: dssmodels.Owner("bob"), + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + }, + } + + s.Cells = make(s2.CellUnion, len(cellIDs)) + for i, id := range cellIDs { + s.Cells[i] = s2.CellID(id) + } + return s + } + + // We should be able to insert 10 subscriptions without error. + for i := 0; i < 10; i++ { + ret, err := insertSubscription(ctx, repo, makeSubscription([]uint64{12494535901059219456, 12494535866699481088})) + require.NoError(t, err) + require.NotNil(t, &ret) + } + + // Inserting the 11th subscription will fail. + ret, err := insertSubscription(ctx, repo, makeSubscription([]uint64{12494535901059219456, 12494535866699481088})) + require.Equal(t, dsserr.Exhausted, stacktrace.GetCode(err)) + require.Nil(t, ret) + + // Inserting a subscription in a different cell will succeed. + ret, err = insertSubscription(ctx, repo, makeSubscription([]uint64{12494535832339742720})) + require.NoError(t, err) + require.NotNil(t, &ret) + + // Inserting a subscription that overlaps fail. + ret, err = insertSubscription(ctx, repo, makeSubscription([]uint64{12494535935418957824, 12494535866699481088})) + require.Equal(t, dsserr.Exhausted, stacktrace.GetCode(err)) + require.Nil(t, ret) +} diff --git a/pkg/rid/server/v1/server_test.go b/pkg/rid/server/v1/server_test.go index 094417cba..a552ea5db 100644 --- a/pkg/rid/server/v1/server_test.go +++ b/pkg/rid/server/v1/server_test.go @@ -43,13 +43,6 @@ type mockApp struct { mock.Mock } -func (ma *mockApp) InsertSubscription(ctx context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) { - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - args := ma.Called(ctx, s) - return args.Get(0).(*ridmodels.Subscription), args.Error(1) -} - func (ma *mockApp) UpdateSubscription(ctx context.Context, s *ridmodels.Subscription) (*ridmodels.Subscription, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() @@ -136,7 +129,7 @@ func TestDeleteSubscription(t *testing.T) { name: "subscription-is-returned-if-returned-from-store", id: dssmodels.ID(uuid.New().String()), version: testdata.Version, - subscription: &ridmodels.Subscription{}, + subscription: &ridmodels.Subscription{CellsVolume4D: &dssmodels.CellsVolume4D{}}, }, { name: "error-is-returned-if-returned-from-store", @@ -192,52 +185,17 @@ func TestCreateSubscription(t *testing.T) { callbacks: restapi.SubscriptionCallbacks{IdentificationServiceAreaUrl: &testdata.CallbackURL}, extents: testdata.LoopVolume4D, wantSubscription: &ridmodels.Subscription{ - ID: "4348c8e5-0b1c-43cf-9114-2e67a4532765", - Owner: "foo", - URL: "https://testdummy.interuss.org/interuss/dss/pkg/geo/testdata/testdata", - StartTime: mustTimestamp(testdata.LoopVolume4D.TimeStart), - EndTime: mustTimestamp(testdata.LoopVolume4D.TimeEnd), - AltitudeHi: (*float32)(testdata.LoopVolume3D.AltitudeHi), - AltitudeLo: (*float32)(testdata.LoopVolume3D.AltitudeLo), - Cells: mustPolygonToCellIDs(&testdata.LoopPolygon), - }, - }, - { - name: "missing-extents", - id: dssmodels.ID("4348c8e5-0b1c-43cf-9114-2e67a4532765"), - callbacks: restapi.SubscriptionCallbacks{IdentificationServiceAreaUrl: &testdata.CallbackURL}, - appErr: dsserr.BadRequest, - wantErr: &respSet.Response400, - }, - { - name: "missing-extents-spatial-volume", - id: dssmodels.ID("4348c8e5-0b1c-43cf-9114-2e67a4532765"), - callbacks: restapi.SubscriptionCallbacks{IdentificationServiceAreaUrl: &testdata.CallbackURL}, - extents: restapi.Volume4D{}, - appErr: dsserr.BadRequest, - wantErr: &respSet.Response400, - }, - { - name: "missing-spatial-volume-footprint", - id: dssmodels.ID("4348c8e5-0b1c-43cf-9114-2e67a4532765"), - callbacks: restapi.SubscriptionCallbacks{IdentificationServiceAreaUrl: &testdata.CallbackURL}, - extents: restapi.Volume4D{ - SpatialVolume: restapi.Volume3D{}, - }, - appErr: dsserr.BadRequest, - wantErr: &respSet.Response400, - }, - { - name: "missing-spatial-volume-footprint", - id: dssmodels.ID("4348c8e5-0b1c-43cf-9114-2e67a4532765"), - callbacks: restapi.SubscriptionCallbacks{IdentificationServiceAreaUrl: &testdata.CallbackURL}, - extents: restapi.Volume4D{ - SpatialVolume: restapi.Volume3D{ - Footprint: restapi.GeoPolygon{}, + ID: "4348c8e5-0b1c-43cf-9114-2e67a4532765", + Owner: "foo", + URL: "https://testdummy.interuss.org/interuss/dss/pkg/geo/testdata/testdata", + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: mustTimestamp(testdata.LoopVolume4D.TimeStart), + EndTime: mustTimestamp(testdata.LoopVolume4D.TimeEnd), + AltitudeHi: (*float32)(testdata.LoopVolume3D.AltitudeHi), + AltitudeLo: (*float32)(testdata.LoopVolume3D.AltitudeLo), + Cells: mustPolygonToCellIDs(&testdata.LoopPolygon), }, }, - appErr: dsserr.BadRequest, - wantErr: &respSet.Response400, }, { name: "missing-callbacks", @@ -249,14 +207,15 @@ func TestCreateSubscription(t *testing.T) { } { t.Run(r.name, func(t *testing.T) { ma := &mockApp{} + ms := &mockStore{} if r.appErr == stacktrace.ErrorCode(0) { ma.On("SearchISAs", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( []*ridmodels.IdentificationServiceArea(nil), nil) - ma.On("InsertSubscription", mock.Anything, r.wantSubscription).Return( + ms.On("Transact", mock.Anything, mock.Anything).Return( r.wantSubscription, nil, ) } - s := &Server{App: ma} + s := &Server{App: ma, Store: ms} respSet = s.CreateSubscription(context.Background(), &restapi.CreateSubscriptionRequest{ Id: restapi.SubscriptionUUID(r.id.String()), @@ -272,6 +231,7 @@ func TestCreateSubscription(t *testing.T) { require.NotNil(t, respSet.Response200) } require.True(t, ma.AssertExpectations(t)) + require.True(t, ms.AssertExpectations(t)) }) } } @@ -287,22 +247,26 @@ func TestCreateSubscriptionResponseIncludesISAs(t *testing.T) { cells := mustPolygonToCellIDs(&testdata.LoopPolygon) sub := &ridmodels.Subscription{ - ID: "4348c8e5-0b1c-43cf-9114-2e67a4532765", - Owner: "foo", - URL: string(testdata.CallbackURL), - StartTime: mustTimestamp(testdata.LoopVolume4D.TimeStart), - EndTime: mustTimestamp(testdata.LoopVolume4D.TimeEnd), - AltitudeHi: (*float32)(testdata.LoopVolume3D.AltitudeHi), - AltitudeLo: (*float32)(testdata.LoopVolume3D.AltitudeLo), - Cells: cells, + ID: "4348c8e5-0b1c-43cf-9114-2e67a4532765", + Owner: "foo", + URL: string(testdata.CallbackURL), + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: mustTimestamp(testdata.LoopVolume4D.TimeStart), + EndTime: mustTimestamp(testdata.LoopVolume4D.TimeEnd), + AltitudeHi: (*float32)(testdata.LoopVolume3D.AltitudeHi), + AltitudeLo: (*float32)(testdata.LoopVolume3D.AltitudeLo), + Cells: cells, + }, } ma := &mockApp{} + ms := &mockStore{} ma.On("SearchISAs", mock.Anything, cells, mock.Anything, mock.Anything).Return(isas, nil) - ma.On("InsertSubscription", mock.Anything, sub).Return(sub, nil) + ms.On("Transact", mock.Anything, mock.Anything).Return(sub, nil) s := &Server{ - App: ma, + App: ma, + Store: ms, } respSet := s.CreateSubscription(context.Background(), &restapi.CreateSubscriptionRequest{ @@ -317,6 +281,7 @@ func TestCreateSubscriptionResponseIncludesISAs(t *testing.T) { }) require.NotNil(t, respSet.Response200) require.True(t, ma.AssertExpectations(t)) + require.True(t, ms.AssertExpectations(t)) require.Equal(t, []restapi.IdentificationServiceArea{ { @@ -338,7 +303,7 @@ func TestGetSubscription(t *testing.T) { { name: "subscription-is-returned-if-returned-from-app", id: dssmodels.ID(uuid.New().String()), - subscription: &ridmodels.Subscription{}, + subscription: &ridmodels.Subscription{CellsVolume4D: &dssmodels.CellsVolume4D{}}, }, { name: "error-is-returned-if-returned-from-app", @@ -418,6 +383,7 @@ func TestSearchSubscriptions(t *testing.T) { Owner: dssmodels.Owner(testdata.Owner), URL: "https://no/place/like/home", NotificationIndex: 42, + CellsVolume4D: &dssmodels.CellsVolume4D{}, }, }, error(nil), ) diff --git a/pkg/rid/server/v1/subscription_handler.go b/pkg/rid/server/v1/subscription_handler.go index 03c931a9e..3c5e64f45 100644 --- a/pkg/rid/server/v1/subscription_handler.go +++ b/pkg/rid/server/v1/subscription_handler.go @@ -10,6 +10,7 @@ import ( dssmodels "github.com/interuss/dss/pkg/models" ridmodels "github.com/interuss/dss/pkg/rid/models" apiv1 "github.com/interuss/dss/pkg/rid/models/api/v1" + "github.com/interuss/dss/pkg/rid/operations" "github.com/interuss/dss/pkg/rid/repos" "github.com/interuss/dss/pkg/store" "github.com/interuss/stacktrace" @@ -140,7 +141,7 @@ func (s *Server) CreateSubscription(ctx context.Context, req *restapi.CreateSubs return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Missing required extents"))}} } - extents, err := apiv1.FromVolume4D(&req.Body.Extents) + cellsExtents, err := apiv1.CellsVolume4DFromRest(&req.Body.Extents) if err != nil { return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Error parsing Volume4D: %v", stacktrace.RootCause(err)))}} @@ -150,28 +151,18 @@ func (s *Server) CreateSubscription(ctx context.Context, req *restapi.CreateSubs return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Invalid ID format"))}} } - + url := string(*req.Body.Callbacks.IdentificationServiceAreaUrl) if !s.AllowHTTPBaseUrls { - err = ridmodels.ValidateURL(string(*req.Body.Callbacks.IdentificationServiceAreaUrl)) + err := ridmodels.ValidateURL(url) if err != nil { return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate IdentificationServiceAreaUrl"))}} } } - sub := &ridmodels.Subscription{ - ID: id, - Owner: dssmodels.Owner(*req.Auth.ClientID), - URL: string(*req.Body.Callbacks.IdentificationServiceAreaUrl), - Writer: s.Locality, - } - - if err := sub.SetExtents(extents); err != nil { - return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ - Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Invalid extents"))}} - } + payload := operations.NewInsertSubscriptionPayload(id, dssmodels.Owner(*req.Auth.ClientID), url, nil, cellsExtents) - insertedSub, err := s.App.InsertSubscription(ctx, sub) + insertedSub, err := store.TransactWithResult[repos.Repository, *ridmodels.Subscription](ctx, s.Store, payload) if err != nil { err = stacktrace.Propagate(err, "Could not insert Subscription") errResp := &restapi.ErrorResponse{Message: dsserr.Handle(ctx, err)} @@ -189,7 +180,7 @@ func (s *Server) CreateSubscription(ctx context.Context, req *restapi.CreateSubs } // Find ISAs that were in this subscription's area. - isas, err := s.App.SearchISAs(ctx, sub.Cells, nil, nil) + isas, err := s.App.SearchISAs(ctx, insertedSub.Cells, nil, nil) if err != nil { err = stacktrace.Propagate(err, "Could not search ISAs") if stacktrace.GetCode(err) == dsserr.BadRequest { @@ -250,11 +241,12 @@ func (s *Server) UpdateSubscription(ctx context.Context, req *restapi.UpdateSubs } sub := &ridmodels.Subscription{ - ID: id, - Owner: dssmodels.Owner(*req.Auth.ClientID), - URL: string(*req.Body.Callbacks.IdentificationServiceAreaUrl), - Version: version, - Writer: s.Locality, + ID: id, + Owner: dssmodels.Owner(*req.Auth.ClientID), + URL: string(*req.Body.Callbacks.IdentificationServiceAreaUrl), + Version: version, + Writer: s.Locality, + CellsVolume4D: &dssmodels.CellsVolume4D{}, } if err := sub.SetExtents(extents); err != nil { diff --git a/pkg/rid/server/v2/subscription_handler.go b/pkg/rid/server/v2/subscription_handler.go index da7ca6431..fa58264ed 100644 --- a/pkg/rid/server/v2/subscription_handler.go +++ b/pkg/rid/server/v2/subscription_handler.go @@ -10,6 +10,7 @@ import ( dssmodels "github.com/interuss/dss/pkg/models" ridmodels "github.com/interuss/dss/pkg/rid/models" apiv2 "github.com/interuss/dss/pkg/rid/models/api/v2" + "github.com/interuss/dss/pkg/rid/operations" "github.com/interuss/dss/pkg/rid/repos" store "github.com/interuss/dss/pkg/store" "github.com/interuss/stacktrace" @@ -136,7 +137,7 @@ func (s *Server) CreateSubscription(ctx context.Context, req *restapi.CreateSubs return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Missing required USS base URL"))}} } - extents, err := apiv2.FromVolume4D(&req.Body.Extents) + cellsExtents, err := apiv2.CellsVolume4DFromRest(&req.Body.Extents) if err != nil { return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Error parsing Volume4D: %v", stacktrace.RootCause(err)))}} @@ -146,28 +147,18 @@ func (s *Server) CreateSubscription(ctx context.Context, req *restapi.CreateSubs return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Invalid ID format"))}} } - + url := string(req.Body.UssBaseUrl) if !s.AllowHTTPBaseUrls { - err = ridmodels.ValidateURL(string(req.Body.UssBaseUrl)) + err := ridmodels.ValidateURL(url) if err != nil { return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate UssBaseUrl"))}} } } - sub := &ridmodels.Subscription{ - ID: id, - Owner: dssmodels.Owner(*req.Auth.ClientID), - URL: string(req.Body.UssBaseUrl), - Writer: s.Locality, - } - - if err := sub.SetExtents(extents); err != nil { - return restapi.CreateSubscriptionResponseSet{Response400: &restapi.ErrorResponse{ - Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Invalid extents"))}} - } + payload := operations.NewInsertSubscriptionPayload(id, dssmodels.Owner(*req.Auth.ClientID), url, nil, cellsExtents) - insertedSub, err := s.App.InsertSubscription(ctx, sub) + insertedSub, err := store.TransactWithResult[repos.Repository, *ridmodels.Subscription](ctx, s.Store, payload) if err != nil { err = stacktrace.Propagate(err, "Could not insert Subscription") errResp := &restapi.ErrorResponse{Message: dsserr.Handle(ctx, err)} @@ -185,7 +176,7 @@ func (s *Server) CreateSubscription(ctx context.Context, req *restapi.CreateSubs } // Find ISAs that were in this subscription's area. - isas, err := s.App.SearchISAs(ctx, sub.Cells, nil, nil) + isas, err := s.App.SearchISAs(ctx, insertedSub.Cells, nil, nil) if err != nil { err = stacktrace.Propagate(err, "Could not search ISAs") if stacktrace.GetCode(err) == dsserr.BadRequest { @@ -242,11 +233,12 @@ func (s *Server) UpdateSubscription(ctx context.Context, req *restapi.UpdateSubs } sub := &ridmodels.Subscription{ - ID: id, - Owner: dssmodels.Owner(*req.Auth.ClientID), - URL: string(req.Body.UssBaseUrl), - Version: version, - Writer: s.Locality, + ID: id, + Owner: dssmodels.Owner(*req.Auth.ClientID), + URL: string(req.Body.UssBaseUrl), + Version: version, + Writer: s.Locality, + CellsVolume4D: &dssmodels.CellsVolume4D{}, } if err := sub.SetExtents(extents); err != nil { diff --git a/pkg/rid/store/memstore/store_test.go b/pkg/rid/store/memstore/store_test.go index 55d13081f..f9cff2eea 100644 --- a/pkg/rid/store/memstore/store_test.go +++ b/pkg/rid/store/memstore/store_test.go @@ -42,8 +42,10 @@ func TestDatabaseEnsuresBeginsBeforeExpires(t *testing.T) { Owner: "me-myself-and-i", URL: "https://no/place/like/home", NotificationIndex: 42, - StartTime: &begins, - EndTime: &expires, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &begins, + EndTime: &expires, + }, }) require.Error(t, err) } diff --git a/pkg/rid/store/memstore/subscriptions.go b/pkg/rid/store/memstore/subscriptions.go index f5a7bffdb..b1381354e 100644 --- a/pkg/rid/store/memstore/subscriptions.go +++ b/pkg/rid/store/memstore/subscriptions.go @@ -41,13 +41,15 @@ func (rec *subscriptionRecord) toModel() *ridmodels.Subscription { URL: rec.URL, NotificationIndex: rec.NotificationIndex, Owner: rec.Owner, - Cells: slices.Clone(rec.Cells), - StartTime: utils.ClonePtr(rec.StartTime), - EndTime: utils.ClonePtr(rec.EndTime), Version: dssmodels.VersionFromTime(rec.UpdatedAt), - AltitudeHi: utils.ClonePtr(rec.AltitudeHi), - AltitudeLo: utils.ClonePtr(rec.AltitudeLo), Writer: rec.Writer, + CellsVolume4D: &dssmodels.CellsVolume4D{ + Cells: slices.Clone(rec.Cells), + StartTime: utils.ClonePtr(rec.StartTime), + EndTime: utils.ClonePtr(rec.EndTime), + AltitudeHi: utils.ClonePtr(rec.AltitudeHi), + AltitudeLo: utils.ClonePtr(rec.AltitudeLo), + }, } } diff --git a/pkg/rid/store/memstore/subscriptions_test.go b/pkg/rid/store/memstore/subscriptions_test.go index 993aaeff8..c1a191a73 100644 --- a/pkg/rid/store/memstore/subscriptions_test.go +++ b/pkg/rid/store/memstore/subscriptions_test.go @@ -28,13 +28,15 @@ var ( ID: dssmodels.ID(uuid.New().String()), Owner: "myself", URL: "https://no/place/like/home", - StartTime: &startTime, - EndTime: &endTime, NotificationIndex: 42, Writer: writer, - Cells: s2.CellUnion{ - s2.CellID(overflow), - 12494535935418957824, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + Cells: s2.CellUnion{ + s2.CellID(overflow), + 12494535935418957824, + }, }, }, }, @@ -44,10 +46,12 @@ var ( ID: dssmodels.ID(uuid.New().String()), Owner: "myself", URL: "https://no/place/like/home", - EndTime: &endTime, NotificationIndex: 42, - Cells: s2.CellUnion{ - 12494535935418957824, + CellsVolume4D: &dssmodels.CellsVolume4D{ + EndTime: &endTime, + Cells: s2.CellUnion{ + 12494535935418957824, + }, }, }, }, @@ -57,11 +61,13 @@ var ( ID: dssmodels.ID(uuid.New().String()), Owner: "me", URL: "https://no/place/like/home", - StartTime: &startTime, - EndTime: &endTime, NotificationIndex: 42, - Cells: s2.CellUnion{ - 12494535935418957824, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + Cells: s2.CellUnion{ + 12494535935418957824, + }, }, }, }, @@ -212,10 +218,12 @@ func TestStoreExpiredSubscription(t *testing.T) { endTime := fakeClock.Now().Add(24 * time.Hour) sub := &ridmodels.Subscription{ - ID: dssmodels.ID(uuid.New().String()), - Owner: dssmodels.Owner("original owner"), - Cells: s2.CellUnion{s2.CellID(12494535866699481088)}, - EndTime: &endTime, + ID: dssmodels.ID(uuid.New().String()), + Owner: dssmodels.Owner("original owner"), + CellsVolume4D: &dssmodels.CellsVolume4D{ + Cells: s2.CellUnion{s2.CellID(12494535866699481088)}, + EndTime: &endTime, + }, } _, err := repo.InsertSubscription(ctx, sub) require.NoError(t, err) @@ -251,9 +259,11 @@ func TestStoreSubscriptionWithNoGeoData(t *testing.T) { endTime := fakeClock.Now().Add(24 * time.Hour) sub := &ridmodels.Subscription{ - ID: dssmodels.ID(uuid.New().String()), - Owner: dssmodels.Owner("original owner"), - EndTime: &endTime, + ID: dssmodels.ID(uuid.New().String()), + Owner: dssmodels.Owner("original owner"), + CellsVolume4D: &dssmodels.CellsVolume4D{ + EndTime: &endTime, + }, } _, err := repo.InsertSubscription(ctx, sub) require.Error(t, err) diff --git a/pkg/rid/store/sqlstore/store_test.go b/pkg/rid/store/sqlstore/store_test.go index bc80862d4..83e77b4e1 100644 --- a/pkg/rid/store/sqlstore/store_test.go +++ b/pkg/rid/store/sqlstore/store_test.go @@ -84,8 +84,10 @@ func TestDatabaseEnsuresBeginsBeforeExpires(t *testing.T) { Owner: "me-myself-and-i", URL: "https://no/place/like/home", NotificationIndex: 42, - StartTime: &begins, - EndTime: &expires, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &begins, + EndTime: &expires, + }, }) require.Error(t, err) } diff --git a/pkg/rid/store/sqlstore/subscriptions.go b/pkg/rid/store/sqlstore/subscriptions.go index 56ab717f6..29fde6ca0 100644 --- a/pkg/rid/store/sqlstore/subscriptions.go +++ b/pkg/rid/store/sqlstore/subscriptions.go @@ -33,7 +33,7 @@ func (r *repo) process(ctx context.Context, query string, args ...interface{}) ( var writer pgtype.Text for rows.Next() { - s := new(ridmodels.Subscription) + s := &ridmodels.Subscription{CellsVolume4D: &dssmodels.CellsVolume4D{}} var updateTime time.Time diff --git a/pkg/rid/store/sqlstore/subscriptions_test.go b/pkg/rid/store/sqlstore/subscriptions_test.go index dd4319a4a..99bd6a161 100644 --- a/pkg/rid/store/sqlstore/subscriptions_test.go +++ b/pkg/rid/store/sqlstore/subscriptions_test.go @@ -27,13 +27,15 @@ var ( ID: dssmodels.ID(uuid.New().String()), Owner: "myself", URL: "https://no/place/like/home", - StartTime: &startTime, - EndTime: &endTime, NotificationIndex: 42, Writer: writer, - Cells: s2.CellUnion{ - s2.CellID(overflow), - 12494535935418957824, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + Cells: s2.CellUnion{ + s2.CellID(overflow), + 12494535935418957824, + }, }, }, }, @@ -43,10 +45,12 @@ var ( ID: dssmodels.ID(uuid.New().String()), Owner: "myself", URL: "https://no/place/like/home", - EndTime: &endTime, NotificationIndex: 42, - Cells: s2.CellUnion{ - 12494535935418957824, + CellsVolume4D: &dssmodels.CellsVolume4D{ + EndTime: &endTime, + Cells: s2.CellUnion{ + 12494535935418957824, + }, }, }, }, @@ -56,11 +60,13 @@ var ( ID: dssmodels.ID(uuid.New().String()), Owner: "me", URL: "https://no/place/like/home", - StartTime: &startTime, - EndTime: &endTime, NotificationIndex: 42, - Cells: s2.CellUnion{ - 12494535935418957824, + CellsVolume4D: &dssmodels.CellsVolume4D{ + StartTime: &startTime, + EndTime: &endTime, + Cells: s2.CellUnion{ + 12494535935418957824, + }, }, }, }, @@ -234,10 +240,12 @@ func TestStoreExpiredSubscription(t *testing.T) { endTime := fakeClock.Now().Add(24 * time.Hour) sub := &ridmodels.Subscription{ - ID: dssmodels.ID(uuid.New().String()), - Owner: dssmodels.Owner("original owner"), - Cells: s2.CellUnion{s2.CellID(12494535866699481088)}, - EndTime: &endTime, + ID: dssmodels.ID(uuid.New().String()), + Owner: dssmodels.Owner("original owner"), + CellsVolume4D: &dssmodels.CellsVolume4D{ + Cells: s2.CellUnion{s2.CellID(12494535866699481088)}, + EndTime: &endTime, + }, } _, err = repo.InsertSubscription(ctx, sub) require.NoError(t, err) @@ -276,9 +284,11 @@ func TestStoreSubscriptionWithNoGeoData(t *testing.T) { endTime := fakeClock.Now().Add(24 * time.Hour) sub := &ridmodels.Subscription{ - ID: dssmodels.ID(uuid.New().String()), - Owner: dssmodels.Owner("original owner"), - EndTime: &endTime, + ID: dssmodels.ID(uuid.New().String()), + Owner: dssmodels.Owner("original owner"), + CellsVolume4D: &dssmodels.CellsVolume4D{ + EndTime: &endTime, + }, } _, err = repo.InsertSubscription(ctx, sub) require.Error(t, err)