diff --git a/apps/gateway/dtos/sync.go b/apps/gateway/dtos/sync.go index 56c01b5..056919f 100644 --- a/apps/gateway/dtos/sync.go +++ b/apps/gateway/dtos/sync.go @@ -6,6 +6,7 @@ type SyncRequestType string var SyncRequestTypes = struct { RoutesV1 SyncRequestType + RoutesV2 SyncRequestType DirectionsV1 SyncRequestType StopsV1 SyncRequestType AlertsV1 SyncRequestType @@ -13,6 +14,7 @@ var SyncRequestTypes = struct { TimetablesV1 SyncRequestType }{ RoutesV1: "RoutesV1", + RoutesV2: "RoutesV2", DirectionsV1: "DirectionsV1", StopsV1: "StopsV1", AlertsV1: "AlertsV1", @@ -26,7 +28,9 @@ type SyncEntityType string var SyncEntityTypes = struct { RouteV1 SyncEntityType + RouteV2 SyncEntityType RouteDeleteV1 SyncEntityType + RouteDeleteV2 SyncEntityType DirectionV1 SyncEntityType DirectionDeleteV1 SyncEntityType StopV1 SyncEntityType @@ -42,7 +46,9 @@ var SyncEntityTypes = struct { SyncCompleteV1 SyncEntityType }{ RouteV1: "RouteV1", + RouteV2: "RouteV2", RouteDeleteV1: "RouteDeleteV1", + RouteDeleteV2: "RouteDeleteV2", DirectionV1: "DirectionV1", DirectionDeleteV1: "DirectionDeleteV1", StopV1: "StopV1", diff --git a/apps/gateway/dtos/sync_payloads.go b/apps/gateway/dtos/sync_payloads.go index d64828b..663240d 100644 --- a/apps/gateway/dtos/sync_payloads.go +++ b/apps/gateway/dtos/sync_payloads.go @@ -11,7 +11,9 @@ import ( // A payload never changes once shipped. A new field means a new version of the entity. var SyncPayloads = map[SyncEntityType]any{ SyncEntityTypes.RouteV1: SyncRouteV1{}, + SyncEntityTypes.RouteV2: SyncRouteV2{}, SyncEntityTypes.RouteDeleteV1: SyncRouteDeleteV1{}, + SyncEntityTypes.RouteDeleteV2: SyncRouteDeleteV1{}, SyncEntityTypes.DirectionV1: SyncDirectionV1{}, SyncEntityTypes.DirectionDeleteV1: SyncDirectionDeleteV1{}, SyncEntityTypes.StopV1: SyncStopV1{}, @@ -52,6 +54,28 @@ func NewSyncRouteV1(r model.Route) SyncRouteV1 { } } +type SyncRouteV2 struct { + ID uuid.UUID `json:"id"` + ShortName string `json:"shortName"` + LongName string `json:"longName"` + LightColor string `json:"lightColor"` + DarkColor string `json:"darkColor"` + Active bool `json:"active"` + IsLoop bool `json:"isLoop"` +} + +func NewSyncRouteV2(r model.Route) SyncRouteV2 { + return SyncRouteV2{ + ID: r.ID, + ShortName: r.ShortName, + LongName: r.LongName, + LightColor: r.LightColor, + DarkColor: r.DarkColor, + Active: r.Active, + IsLoop: r.IsLoop, + } +} + type SyncRouteDeleteV1 struct { RouteID uuid.UUID `json:"routeId"` } diff --git a/apps/gateway/services/sync.go b/apps/gateway/services/sync.go index cc8c830..22cac4c 100644 --- a/apps/gateway/services/sync.go +++ b/apps/gateway/services/sync.go @@ -52,6 +52,7 @@ type SyncStream struct { // Parents come before children so a client can apply lines in the order they arrive. var SyncStreams = []SyncStream{ NewSyncStream(dtos.SyncRequestTypes.RoutesV1, dtos.SyncEntityTypes.RouteV1, dtos.SyncEntityTypes.RouteDeleteV1, dtos.NewSyncRouteV1, dtos.NewSyncRouteDeleteV1), + NewSyncStream(dtos.SyncRequestTypes.RoutesV2, dtos.SyncEntityTypes.RouteV2, dtos.SyncEntityTypes.RouteDeleteV2, dtos.NewSyncRouteV2, dtos.NewSyncRouteDeleteV1), NewSyncStream(dtos.SyncRequestTypes.DirectionsV1, dtos.SyncEntityTypes.DirectionV1, dtos.SyncEntityTypes.DirectionDeleteV1, dtos.NewSyncDirectionV1, dtos.NewSyncDirectionDeleteV1), NewSyncStream(dtos.SyncRequestTypes.StopsV1, dtos.SyncEntityTypes.StopV1, dtos.SyncEntityTypes.StopDeleteV1, dtos.NewSyncStopV1, dtos.NewSyncStopDeleteV1), NewSyncStream(dtos.SyncRequestTypes.AlertsV1, dtos.SyncEntityTypes.AlertV1, dtos.SyncEntityTypes.AlertDeleteV1, dtos.NewSyncAlertV1, dtos.NewSyncAlertDeleteV1), diff --git a/apps/gateway/services/sync_test.go b/apps/gateway/services/sync_test.go index 275dd32..a021ac8 100644 --- a/apps/gateway/services/sync_test.go +++ b/apps/gateway/services/sync_test.go @@ -131,22 +131,19 @@ var _ = Describe("SyncService.Plan", Label("unit"), func() { }) When("a second version of a resource exists", func() { - BeforeEach(func() { - original := services.SyncStreams - routesV2 := services.NewSyncStream("RoutesV2", "RouteV2", "RouteDeleteV2", dtos.NewSyncRouteV1, dtos.NewSyncRouteDeleteV1) - services.SyncStreams = append(slices.Clone(original), routesV2) - DeferCleanup(func() { services.SyncStreams = original }) - }) - It("rejects both versions in one request", func() { - _, err := svc.Plan(dtos.SyncRequest{Protocol: dtos.SyncProtocolVersions.V1, Types: []dtos.SyncRequestType{requests.RoutesV1, "RoutesV2"}}) + _, err := svc.Plan(dtos.SyncRequest{Protocol: dtos.SyncProtocolVersions.V1, Types: []dtos.SyncRequestType{requests.RoutesV1, requests.RoutesV2}}) Expect(err).To(MatchError(services.ErrInvalidSyncRequest)) }) - It("accepts either version alone", func() { - _, err := svc.Plan(dtos.SyncRequest{Protocol: dtos.SyncProtocolVersions.V1, Types: []dtos.SyncRequestType{"RoutesV2"}}) - Expect(err).NotTo(HaveOccurred()) - }) + DescribeTable("accepts either version alone", + func(request dtos.SyncRequestType) { + _, err := svc.Plan(dtos.SyncRequest{Protocol: dtos.SyncProtocolVersions.V1, Types: []dtos.SyncRequestType{request}}) + Expect(err).NotTo(HaveOccurred()) + }, + Entry("RoutesV1", requests.RoutesV1), + Entry("RoutesV2", requests.RoutesV2), + ) }) DescribeTable("rejects bad requests", diff --git a/apps/requester/repositories/route_data.go b/apps/requester/repositories/route_data.go index 485c7c4..90cc919 100644 --- a/apps/requester/repositories/route_data.go +++ b/apps/requester/repositories/route_data.go @@ -138,9 +138,9 @@ func (r *RouteDataRepository) UpsertRoutes(ctx context.Context, routes []model.R return upsertAll(ctx, r.db, routes, func(route model.Route) string { return route.SourceID }, upsertSpec{ - columns: []string{"sourceId", "shortName", "longName", "lightColor", "darkColor"}, + columns: []string{"sourceId", "shortName", "longName", "lightColor", "darkColor", "isLoop"}, conflict: `CONFLICT ("shortName") DO UPDATE`, - set: `"sourceId" = EXCLUDED."sourceId", "longName" = EXCLUDED."longName", "lightColor" = EXCLUDED."lightColor", "darkColor" = EXCLUDED."darkColor", "active" = TRUE, "deactivatedAt" = NULL`, + set: `"sourceId" = EXCLUDED."sourceId", "longName" = EXCLUDED."longName", "lightColor" = EXCLUDED."lightColor", "darkColor" = EXCLUDED."darkColor", "isLoop" = EXCLUDED."isLoop", "active" = TRUE, "deactivatedAt" = NULL`, }) } diff --git a/apps/requester/services/route_data.go b/apps/requester/services/route_data.go index de20cc6..6611be5 100644 --- a/apps/requester/services/route_data.go +++ b/apps/requester/services/route_data.go @@ -1,9 +1,11 @@ package services import ( + "cmp" "context" "fmt" "log/slog" + "slices" "strconv" "strings" @@ -17,6 +19,11 @@ import ( "github.com/MaroonRides/api/internal/db/model" ) +// Game day routes run out to the stadium and back once, so their last stop does not lead into the first. +const gamedayRoutePrefix = "Gameday" + +var gamePhases = []string{"pregame", "postgame"} + type RouteDataService struct { api *busapi.Client repo *repositories.RouteDataRepository @@ -137,13 +144,14 @@ func routeRows(routes []busapi.MapRoute) []model.Route { LongName: route.Name, LightColor: strings.ToLower(lightColor), DarkColor: strings.ToLower(darkColor), + IsLoop: !strings.HasPrefix(route.Name, gamedayRoutePrefix), }) } return out } -// destination is the headsign and the order a direction is listed in, which +// destination is the headsign and the order a direction is shown in, which // only the base data carries. type destination struct { text string @@ -154,7 +162,12 @@ func destinations(routes []busapi.MapRoute) map[string]destination { out := map[string]destination{} for _, route := range routes { - for sequence, direction := range route.DirectionList { + listed := slices.Clone(route.DirectionList) + slices.SortStableFunc(listed, func(a, b busapi.MapDirectionList) int { + return cmp.Compare(gamePhase(a.Destination), gamePhase(b.Destination)) + }) + + for sequence, direction := range listed { out[direction.Direction.Key] = destination{ text: direction.Destination, sequence: sequence, @@ -165,6 +178,11 @@ func destinations(routes []busapi.MapRoute) map[string]destination { return out } +func gamePhase(destination string) int { + lower := strings.ToLower(destination) + return slices.IndexFunc(gamePhases, func(phase string) bool { return strings.Contains(lower, phase) }) +} + func directionRows(paths []busapi.PatternPathsResponse, routes map[string]model.Route, headsigns map[string]destination) []model.Direction { var out []model.Direction diff --git a/apps/requester/services/route_data_test.go b/apps/requester/services/route_data_test.go index 71f5ccf..5338396 100644 --- a/apps/requester/services/route_data_test.go +++ b/apps/requester/services/route_data_test.go @@ -33,6 +33,47 @@ var _ = Describe("stopRows", Label("unit"), func() { }) }) +var _ = Describe("routeRows", Label("unit"), func() { + It("marks game day routes as not looping", func() { + listed := []busapi.MapDirectionList{{LineColor: "#500000"}} + + rows := routeRows([]busapi.MapRoute{ + {Key: "bonfire", Name: "Bonfire", ShortName: "01", DirectionList: listed}, + {Key: "gameday", Name: "Gameday Bonfire", ShortName: "BF", DirectionList: listed}, + }) + + Expect(rows).To(HaveLen(2)) + Expect(rows[0].IsLoop).To(BeTrue()) + Expect(rows[1].IsLoop).To(BeFalse()) + }) +}) + +var _ = Describe("destinations", Label("unit"), func() { + It("puts pregame before postgame", func() { + got := destinations([]busapi.MapRoute{{DirectionList: []busapi.MapDirectionList{ + {Direction: busapi.MapDirection{Key: "post"}, Destination: "1 Hour Postgame"}, + {Direction: busapi.MapDirection{Key: "pre"}, Destination: "3 Hours Pregame"}, + }}}) + + Expect(got).To(Equal(map[string]destination{ + "pre": {text: "3 Hours Pregame", sequence: 0}, + "post": {text: "1 Hour Postgame", sequence: 1}, + })) + }) + + It("keeps the upstream order of other directions", func() { + got := destinations([]busapi.MapRoute{{DirectionList: []busapi.MapDirectionList{ + {Direction: busapi.MapDirection{Key: "msc"}, Destination: "MSC"}, + {Direction: busapi.MapDirection{Key: "commons"}, Destination: "Commons"}, + }}}) + + Expect(got).To(Equal(map[string]destination{ + "msc": {text: "MSC", sequence: 0}, + "commons": {text: "Commons", sequence: 1}, + })) + }) +}) + var _ = Describe("directionRows", Label("unit"), func() { It("skips pattern paths for directions missing from base data", func() { route := model.Route{ID: uuid.New(), SourceID: "03"} diff --git a/docs/design/sync-stream.md b/docs/design/sync-stream.md index 69ecd24..cfaa401 100644 --- a/docs/design/sync-stream.md +++ b/docs/design/sync-stream.md @@ -64,6 +64,7 @@ There are two enums, same as Immich. | Request type | Upsert entity | Delete entity | Table | | ------------------- | ------------------ | ------------------------ | ----------------- | | `RoutesV1` | `RouteV1` | `RouteDeleteV1` | `route` | +| `RoutesV2` | `RouteV2` | `RouteDeleteV2` | `route` | | `DirectionsV1` | `DirectionV1` | `DirectionDeleteV1` | `direction` | | `StopsV1` | `StopV1` | `StopDeleteV1` | `stop` | | `AlertsV1` | `AlertV1` | `AlertDeleteV1` | `alert` | diff --git a/internal/db/migrations/20261114195046_route_is_loop.sql b/internal/db/migrations/20261114195046_route_is_loop.sql new file mode 100644 index 0000000..ff87b2f --- /dev/null +++ b/internal/db/migrations/20261114195046_route_is_loop.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- modify "route" table +-- existing rows start as loops; the next route data sync marks the game day routes +ALTER TABLE "route" ADD COLUMN "isLoop" boolean NOT NULL DEFAULT true; +ALTER TABLE "route" ALTER COLUMN "isLoop" DROP DEFAULT; + +-- +goose Down +-- the bump trigger compares isLoop; reconcile recreates it after migrating +DROP TRIGGER IF EXISTS route_sync_bump ON "route"; +-- reverse: modify "route" table +ALTER TABLE "route" DROP COLUMN "isLoop"; diff --git a/internal/db/migrations/atlas.sum b/internal/db/migrations/atlas.sum index b6f07c8..5b1befb 100644 --- a/internal/db/migrations/atlas.sum +++ b/internal/db/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:EJLq2XfT9ptUxtMmZHdrvlC/S1h5oUGiCYiv946O4bE= +h1:oI+ueGWwNfo+sZERLbNmteLtcZOujaRFPPEPKW1PE90= 20260927192657_initial_tables.sql h1:MvX5nd5hspGOKgoPp5crmIjsXj++Bik6/7p5g2deFqo= 20260927202630_passenger_count.sql h1:wrAWgyra+WZe0q0DxXT8c8K87Jgsbgtp3SnrFX+/hcQ= 20260927222835_stop_schedule.sql h1:9CXXfeiycy5z/XG2sjzhHGuhBfvADJy2oovAiWuE1aQ= @@ -11,3 +11,4 @@ h1:EJLq2XfT9ptUxtMmZHdrvlC/S1h5oUGiCYiv946O4bE= 20261001001300_drop_timetable_scope.sql h1:+EKbylBAop6KE4L/gjwq7erjwWtkqOixMjJbZdITbSw= 20261001164832_direction_stop_timepoint_default.sql h1:DLlPM/Qb60yBstp74J+TPnZGVJRfnNGXAyHiLhItMzg= 20261002042012_stops_per_direction.sql h1:uF7e1xHYwpgkecuaMxuoFVLj+u/1NhHejwlzLddM+AA= +20261114195046_route_is_loop.sql h1:Ahi44xWzIVdbm8d5PbT5Wp8JB7WuDOuTDzHJKSRzlkk= diff --git a/internal/db/model/route.go b/internal/db/model/route.go index fccc9cc..05462a2 100644 --- a/internal/db/model/route.go +++ b/internal/db/model/route.go @@ -20,6 +20,7 @@ type Route struct { LongName string `bun:"longName,notnull"` LightColor string `bun:"lightColor,notnull"` DarkColor string `bun:"darkColor,notnull"` + IsLoop bool `bun:"isLoop,notnull"` Active bool `bun:"active,notnull,default:true"` DeactivatedAt *time.Time `bun:"deactivatedAt" sync:"nosync"` diff --git a/openapi.json b/openapi.json index 1aa12a9..9d4f8a5 100644 --- a/openapi.json +++ b/openapi.json @@ -252,6 +252,26 @@ ], "type": "object" }, + "RouteDeleteV2Line": { + "properties": { + "ack": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/SyncRouteDeleteV1" + }, + "type": { + "const": "RouteDeleteV2", + "type": "string" + } + }, + "required": [ + "type", + "ack", + "data" + ], + "type": "object" + }, "RouteV1Line": { "properties": { "ack": { @@ -272,6 +292,26 @@ ], "type": "object" }, + "RouteV2Line": { + "properties": { + "ack": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/SyncRouteV2" + }, + "type": { + "const": "RouteV2", + "type": "string" + } + }, + "required": [ + "type", + "ack", + "data" + ], + "type": "object" + }, "StopDeleteV1Line": { "properties": { "ack": { @@ -487,7 +527,9 @@ "description": "SyncEntityType schema", "enum": [ "RouteV1", + "RouteV2", "RouteDeleteV1", + "RouteDeleteV2", "DirectionV1", "DirectionDeleteV1", "StopV1", @@ -672,6 +714,7 @@ "SyncRequestType": { "enum": [ "RoutesV1", + "RoutesV2", "DirectionsV1", "StopsV1", "AlertsV1", @@ -749,6 +792,43 @@ ], "type": "object" }, + "SyncRouteV2": { + "description": "SyncRouteV2 schema", + "properties": { + "active": { + "type": "boolean" + }, + "darkColor": { + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "isLoop": { + "type": "boolean" + }, + "lightColor": { + "type": "string" + }, + "longName": { + "type": "string" + }, + "shortName": { + "type": "string" + } + }, + "required": [ + "active", + "darkColor", + "id", + "isLoop", + "lightColor", + "longName", + "shortName" + ], + "type": "object" + }, "SyncStopDeleteV1": { "description": "SyncStopDeleteV1 schema", "properties": { @@ -822,7 +902,9 @@ "DirectionDeleteV1": "#/components/schemas/DirectionDeleteV1Line", "DirectionV1": "#/components/schemas/DirectionV1Line", "RouteDeleteV1": "#/components/schemas/RouteDeleteV1Line", + "RouteDeleteV2": "#/components/schemas/RouteDeleteV2Line", "RouteV1": "#/components/schemas/RouteV1Line", + "RouteV2": "#/components/schemas/RouteV2Line", "StopDeleteV1": "#/components/schemas/StopDeleteV1Line", "StopV1": "#/components/schemas/StopV1Line", "SyncCompleteV1": "#/components/schemas/SyncCompleteV1Line", @@ -836,9 +918,15 @@ { "$ref": "#/components/schemas/RouteV1Line" }, + { + "$ref": "#/components/schemas/RouteV2Line" + }, { "$ref": "#/components/schemas/RouteDeleteV1Line" }, + { + "$ref": "#/components/schemas/RouteDeleteV2Line" + }, { "$ref": "#/components/schemas/DirectionV1Line" }, diff --git a/test/factory.go b/test/factory.go index b929d90..fff8c67 100644 --- a/test/factory.go +++ b/test/factory.go @@ -30,6 +30,7 @@ func CreateNetwork(bundb *bun.DB, name string) Network { LongName: "Route " + name, LightColor: "#500000", DarkColor: "#ffffff", + IsLoop: true, Active: true, }, Alert: model.Alert{ diff --git a/test/medium/controllers/sync_controller_test.go b/test/medium/controllers/sync_controller_test.go index 07ff061..c3186c5 100644 --- a/test/medium/controllers/sync_controller_test.go +++ b/test/medium/controllers/sync_controller_test.go @@ -89,6 +89,24 @@ var _ = Describe("SyncController", Label("medium", "controller"), func() { Expect(lines[2].Data).To(BeEmpty()) }) + It("leaves isLoop out of the RoutesV1 lines older apps read", func() { + test.CreateNetwork(bundb, "01") + time.Sleep(2 * repositories.NowIDLag) + + res := post(`{"protocol":1,"types":["RoutesV1"],"acks":[]}`) + + Expect(res.Code).To(Equal(http.StatusOK), res.Body.String()) + lines := readLines(res.Body) + Expect(lines[0].Type).To(Equal(dtos.SyncEntityTypes.RouteV1)) + Expect(lines[0].Data).NotTo(HaveKey("isLoop")) + }) + + It("rejects both versions of routes in one request", func() { + res := post(`{"protocol":1,"types":["RoutesV1","RoutesV2"],"acks":[]}`) + + Expect(res.Code).To(Equal(http.StatusBadRequest), res.Body.String()) + }) + It("streams the timetables of every route", func() { first := test.CreateNetwork(bundb, "01") second := test.CreateNetwork(bundb, "02") diff --git a/test/medium/repositories/route_data_repository_test.go b/test/medium/repositories/route_data_repository_test.go index ee05618..6ba6d6c 100644 --- a/test/medium/repositories/route_data_repository_test.go +++ b/test/medium/repositories/route_data_repository_test.go @@ -100,3 +100,44 @@ var _ = Describe("RouteDataRepository.SyncDirections", Label("medium", "reposito Expect(storedDirectionIDs()).NotTo(ContainElement(synced.Direction.ID)) }) }) + +var _ = Describe("RouteDataRepository.UpsertRoutes", Label("medium", "repository"), func() { + var ( + bundb *bun.DB + repo *requester.RouteDataRepository + ctx = context.Background() + ) + + BeforeEach(func() { + bundb = test.Configure() + repo = requester.NewRouteDataRepository(bundb) + }) + + storedIsLoop := func(shortName string) bool { + var isLoop bool + err := bundb.NewSelect().Model((*model.Route)(nil)).Column("isLoop").Where(`"shortName" = ?`, shortName).Scan(ctx, &isLoop) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + return isLoop + } + + gameday := model.Route{SourceID: "gameday", ShortName: "BF", LongName: "Gameday Bonfire", LightColor: "#500000", DarkColor: "#ffffff"} + + It("stores a route that does not loop", func() { + _, err := repo.UpsertRoutes(ctx, []model.Route{gameday}) + Expect(err).NotTo(HaveOccurred()) + + Expect(storedIsLoop("BF")).To(BeFalse()) + }) + + It("updates whether a stored route loops", func() { + looping := gameday + looping.IsLoop = true + _, err := repo.UpsertRoutes(ctx, []model.Route{looping}) + Expect(err).NotTo(HaveOccurred()) + + _, err = repo.UpsertRoutes(ctx, []model.Route{gameday}) + Expect(err).NotTo(HaveOccurred()) + + Expect(storedIsLoop("BF")).To(BeFalse()) + }) +}) diff --git a/test/medium/services/sync_service_test.go b/test/medium/services/sync_service_test.go index ae3516c..d0a14a7 100644 --- a/test/medium/services/sync_service_test.go +++ b/test/medium/services/sync_service_test.go @@ -184,6 +184,30 @@ var _ = Describe("SyncService", Label("medium", "service"), func() { }) }) + Describe("routes v2", func() { + var network test.Network + + BeforeEach(func() { + network = test.CreateNetwork(bundb, "01") + test.Exec(bundb, `UPDATE "route" SET "isLoop" = FALSE WHERE "id" = ?`, network.Route.ID) + }) + + It("sends whether the route loops", func() { + lines := client.sync(requests.RoutesV2) + + Expect(types(lines)).To(Equal([]dtos.SyncEntityType{entities.RouteV2, entities.SyncCompleteV1})) + Expect(lines[0].Data.(dtos.SyncRouteV2).ID).To(Equal(network.Route.ID)) + Expect(lines[0].Data.(dtos.SyncRouteV2).IsLoop).To(BeFalse()) + }) + + It("keeps serving RoutesV1 to older apps", func() { + lines := client.sync(requests.RoutesV1) + + Expect(types(lines)).To(Equal([]dtos.SyncEntityType{entities.RouteV1, entities.SyncCompleteV1})) + Expect(lines[0].Data).To(BeAssignableToTypeOf(dtos.SyncRouteV1{})) + }) + }) + Describe("a later sync", func() { var first test.Network @@ -437,23 +461,14 @@ gateway_syncs_total{kind="reset"} 1 }) When("a new version of a resource ships", func() { - const routesV2, routeV2 = dtos.SyncRequestType("RoutesV2"), dtos.SyncEntityType("RouteV2") - - BeforeEach(func() { - original := services.SyncStreams - services.SyncStreams = append(slices.Clone(original), - services.NewSyncStream(routesV2, routeV2, "RouteDeleteV2", dtos.NewSyncRouteV1, dtos.NewSyncRouteDeleteV1)) - DeferCleanup(func() { services.SyncStreams = original }) - }) - It("backfills every row for a client that only holds V1 acks", func() { network := test.CreateNetwork(bundb, "01") client.sync(requests.RoutesV1) - lines := client.stream(dtos.SyncRequest{Protocol: dtos.SyncProtocolVersions.V1, Types: []dtos.SyncRequestType{routesV2}, Acks: client.storedAcks()}) + lines := client.stream(dtos.SyncRequest{Protocol: dtos.SyncProtocolVersions.V1, Types: []dtos.SyncRequestType{requests.RoutesV2}, Acks: client.storedAcks()}) - Expect(types(lines)).To(Equal([]dtos.SyncEntityType{routeV2, entities.SyncCompleteV1})) - Expect(lines[0].Data.(dtos.SyncRouteV1).ID).To(Equal(network.Route.ID)) + Expect(types(lines)).To(Equal([]dtos.SyncEntityType{entities.RouteV2, entities.SyncCompleteV1})) + Expect(lines[0].Data.(dtos.SyncRouteV2).ID).To(Equal(network.Route.ID)) }) It("ignores V1 delete acks and sends no tombstones", func() { @@ -464,10 +479,10 @@ gateway_syncs_total{kind="reset"} 1 client.sync(requests.RoutesV1) Expect(client.acks).To(HaveKey(entities.RouteDeleteV1)) - lines := client.stream(dtos.SyncRequest{Protocol: dtos.SyncProtocolVersions.V1, Types: []dtos.SyncRequestType{routesV2}, Acks: client.storedAcks()}) + lines := client.stream(dtos.SyncRequest{Protocol: dtos.SyncProtocolVersions.V1, Types: []dtos.SyncRequestType{requests.RoutesV2}, Acks: client.storedAcks()}) - Expect(types(lines)).To(Equal([]dtos.SyncEntityType{routeV2, entities.SyncCompleteV1})) - Expect(lines[0].Data.(dtos.SyncRouteV1).ID).To(Equal(kept.Route.ID)) + Expect(types(lines)).To(Equal([]dtos.SyncEntityType{entities.RouteV2, entities.SyncCompleteV1})) + Expect(lines[0].Data.(dtos.SyncRouteV2).ID).To(Equal(kept.Route.ID)) }) })