internal/repository/publish_group_postgres_test.go
Rama main · réplica de lectura del repositorio original.
// Integration tests for publish_group persistence: the column is written by
// both insert paths (plain publish, rollout publish) and surfaced by the
// branch listing. Same TEST_DATABASE_URL gating as the rollout store tests.
package repository_test
import (
"context"
"strconv"
"testing"
"xprem/internal/types"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// checkedUpdate publishes and checks one update, stamping a stored uuid so the
// listing resolves it without reaching for bucket metadata.
func (f *rolloutFixture) checkedUpdate(t *testing.T, updateId int64, platform types.Platform, publishGroup *string) {
t.Helper()
ctx := context.Background()
created, err := f.updates.CreateUpdate(ctx, f.appId, updateId, rolloutTestDefaultBranch, rolloutTestRuntime, platform, "abc123", "", publishGroup)
require.NoError(t, err)
require.NoError(t, f.updates.MarkUpdateAsChecked(ctx, *created))
require.NoError(t, f.updates.StoreUpdateUUIDInMetadata(ctx, *created, uuid.NewString()))
}
func TestPublishGroupPersistencePostgres(t *testing.T) {
fixture := newRolloutFixture(t)
ctx := context.Background()
// One publish run: two platform rows sharing the CLI-minted group. A row
// published without one (older CLI) stays ungrouped.
publishGroup := uuid.NewString()
fixture.checkedUpdate(t, 100, "ios", &publishGroup)
fixture.checkedUpdate(t, 200, "android", &publishGroup)
fixture.checkedUpdate(t, 300, "ios", nil)
// Rollbacks are branch-level operations, never grouped: the marker row
// must list with no publish group.
rollback, err := fixture.updates.CreateRollback(ctx, fixture.appId, 400, rolloutTestDefaultBranch, rolloutTestRuntime, "android", "abc123", "")
require.NoError(t, err)
require.NoError(t, fixture.updates.MarkUpdateAsChecked(ctx, *rollback))
// A rollout publish goes through the dedicated insert; group stored the same.
rolloutGroup := uuid.NewString()
rolloutUpdate, err := fixture.updates.CreateUpdateWithRollout(ctx, fixture.appId, 500, rolloutTestDefaultBranch, rolloutTestRuntime, "ios", "abc123", "", 25, &rolloutGroup)
require.NoError(t, err)
require.NoError(t, fixture.updates.MarkUpdateAsChecked(ctx, *rolloutUpdate))
require.NoError(t, fixture.updates.StoreUpdateUUIDInMetadata(ctx, *rolloutUpdate, uuid.NewString()))
page, err := fixture.updates.GetUpdatesByRunTimeVersionAndBranchName(ctx, fixture.appId, rolloutTestRuntime, rolloutTestDefaultBranch, nil, 100)
require.NoError(t, err)
items := page.Items
require.Len(t, items, 5)
groupsById := map[string]*string{}
for _, item := range items {
groupsById[item.UpdateId] = item.PublishGroup
}
require.NotNil(t, groupsById["100"])
require.NotNil(t, groupsById["200"])
assert.Equal(t, publishGroup, *groupsById["100"])
assert.Equal(t, publishGroup, *groupsById["200"])
assert.Nil(t, groupsById["300"])
assert.Nil(t, groupsById["400"])
require.NotNil(t, groupsById["500"])
assert.Equal(t, rolloutGroup, *groupsById["500"])
}
func TestGetUpdatesByPublishGroupPostgres(t *testing.T) {
fixture := newRolloutFixture(t)
ctx := context.Background()
group := uuid.NewString()
fixture.checkedUpdate(t, 100, "ios", &group)
fixture.checkedUpdate(t, 200, "android", &group)
// An unchecked member is an unfinished upload: invisible to group operations.
_, err := fixture.updates.CreateUpdate(ctx, fixture.appId, 300, rolloutTestDefaultBranch, rolloutTestRuntime, "ios", "abc123", "", &group)
require.NoError(t, err)
// Another group on the same branch stays out of the result.
otherGroup := uuid.NewString()
fixture.checkedUpdate(t, 400, "ios", &otherGroup)
members, err := fixture.updates.GetUpdatesByPublishGroup(ctx, fixture.appId, rolloutTestDefaultBranch, rolloutTestRuntime, group)
require.NoError(t, err)
require.Len(t, members, 2)
assert.Equal(t, "100", members[0].UpdateId)
assert.Equal(t, types.PlatformIOS, members[0].Platform)
assert.Equal(t, "abc123", members[0].CommitHash)
assert.Equal(t, "200", members[1].UpdateId)
assert.Equal(t, types.PlatformAndroid, members[1].Platform)
none, err := fixture.updates.GetUpdatesByPublishGroup(ctx, fixture.appId, rolloutTestDefaultBranch, rolloutTestRuntime, uuid.NewString())
require.NoError(t, err)
assert.Empty(t, none)
}
func TestGetPublishGroupsPagePostgres(t *testing.T) {
fixture := newRolloutFixture(t)
ctx := context.Background()
groups := []string{uuid.NewString(), uuid.NewString(), uuid.NewString()}
for index, group := range groups {
baseID := int64((index + 1) * 100)
fixture.checkedUpdate(t, baseID+1, "ios", &group)
fixture.checkedUpdate(t, baseID+2, "android", &group)
}
// Neither legacy ungrouped updates nor unfinished groups are list entries.
fixture.checkedUpdate(t, 450, "android", nil)
unfinishedGroup := uuid.NewString()
_, err := fixture.updates.CreateUpdate(ctx, fixture.appId, 500, rolloutTestDefaultBranch, rolloutTestRuntime, "ios", "abc123", "", &unfinishedGroup)
require.NoError(t, err)
firstPage, err := fixture.updates.GetPublishGroupsPage(ctx, fixture.appId, rolloutTestDefaultBranch, rolloutTestRuntime, nil, 2)
require.NoError(t, err)
require.Len(t, firstPage.Items, 2)
require.NotNil(t, firstPage.NextCursor)
assert.Equal(t, groups[2], firstPage.Items[0].PublishGroup)
assert.Equal(t, []string{"ios", "android"}, firstPage.Items[0].Platforms)
assert.Len(t, firstPage.Items[0].Updates, 2)
assert.Equal(t, groups[1], firstPage.Items[1].PublishGroup)
cursor, err := strconv.ParseInt(*firstPage.NextCursor, 10, 64)
require.NoError(t, err)
secondPage, err := fixture.updates.GetPublishGroupsPage(ctx, fixture.appId, rolloutTestDefaultBranch, rolloutTestRuntime, &cursor, 2)
require.NoError(t, err)
require.Len(t, secondPage.Items, 1)
assert.Nil(t, secondPage.NextCursor)
assert.Equal(t, groups[0], secondPage.Items[0].PublishGroup)
}
func TestGetUpdateFeedPostgres(t *testing.T) {
fixture := newRolloutFixture(t)
ctx := context.Background()
group := uuid.NewString()
fixture.checkedUpdate(t, 100, "ios", &group)
fixture.checkedUpdate(t, 200, "android", &group)
next, err := fixture.updates.CreateUpdate(ctx, fixture.appId, 300, rolloutTestRolloutBranch, rolloutTestRuntime, "ios", "def456", "next branch", nil)
require.NoError(t, err)
require.NoError(t, fixture.updates.MarkUpdateAsChecked(ctx, *next))
nextUUID := uuid.NewString()
require.NoError(t, fixture.updates.StoreUpdateUUIDInMetadata(ctx, *next, nextUUID))
_, err = fixture.updates.CreateUpdate(ctx, fixture.appId, 400, rolloutTestDefaultBranch, rolloutTestRuntime, "ios", "unfinished", "", nil)
require.NoError(t, err)
items, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, types.UpdateFeedQuery{Limit: 100})
require.NoError(t, err)
require.Len(t, items, 3, "unchecked uploads must stay out of the dashboard feed")
byID := make(map[string]types.UpdateFeedItem, len(items))
for _, item := range items {
byID[item.UpdateId] = item
}
assert.Equal(t, rolloutTestDefaultBranch, byID["100"].Branch)
assert.Equal(t, rolloutTestRuntime, byID["100"].RuntimeVersion)
require.NotNil(t, byID["100"].PublishGroup)
assert.Equal(t, group, *byID["100"].PublishGroup)
assert.Equal(t, rolloutTestRolloutBranch, byID["300"].Branch)
assert.Equal(t, nextUUID, byID["300"].UpdateUUID)
assert.Equal(t, "next branch", byID["300"].Message)
groupItems, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, types.UpdateFeedQuery{
PublishGroup: group,
Limit: 100,
})
require.NoError(t, err)
require.Len(t, groupItems, 2)
firstPage, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, types.UpdateFeedQuery{Limit: 2})
require.NoError(t, err)
require.Len(t, firstPage, 2)
cursorUpdateID, err := strconv.ParseInt(firstPage[1].UpdateId, 10, 64)
require.NoError(t, err)
secondPage, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, types.UpdateFeedQuery{
CursorCreatedAt: &firstPage[1].FeedCreatedAt,
CursorBranchID: firstPage[1].BranchID,
CursorUpdateID: cursorUpdateID,
Limit: 2,
})
require.NoError(t, err)
require.Len(t, secondPage, 1)
assert.NotEqual(t, firstPage[0].UpdateId, secondPage[0].UpdateId)
assert.NotEqual(t, firstPage[1].UpdateId, secondPage[0].UpdateId)
}
func TestGetLatestUpdateFeedPostgres(t *testing.T) {
fixture := newRolloutFixture(t)
ctx := context.Background()
fixture.checkedUpdate(t, 100, types.PlatformIOS, nil)
fixture.checkedUpdate(t, 200, types.PlatformAndroid, nil)
rollback, err := fixture.updates.CreateRollback(ctx, fixture.appId, 300, rolloutTestDefaultBranch, rolloutTestRuntime, types.PlatformIOS, "rollback", "back to embedded")
require.NoError(t, err)
require.NoError(t, fixture.updates.MarkUpdateAsChecked(ctx, *rollback))
// Uploading a newer row must not hide the last checked serving head.
_, err = fixture.updates.CreateUpdate(ctx, fixture.appId, 400, rolloutTestDefaultBranch, rolloutTestRuntime, types.PlatformIOS, "unfinished", "", nil)
require.NoError(t, err)
newRuntime := "2.0.0"
_, err = fixture.branches.CreateRuntimeVersion(ctx, fixture.appId, newRuntime)
require.NoError(t, err)
newer, err := fixture.updates.CreateUpdate(ctx, fixture.appId, 500, rolloutTestDefaultBranch, newRuntime, types.PlatformAndroid, "new-runtime", "", nil)
require.NoError(t, err)
require.NoError(t, fixture.updates.MarkUpdateAsChecked(ctx, *newer))
require.NoError(t, fixture.updates.StoreUpdateUUIDInMetadata(ctx, *newer, uuid.NewString()))
fixture.createUpdate(t, rolloutTestRolloutBranch, 300, types.PlatformIOS, true)
foreign := newRolloutFixture(t)
foreign.checkedUpdate(t, 300, types.PlatformIOS, nil)
query := types.UpdateFeedQuery{Branch: rolloutTestDefaultBranch, LatestOnly: true, Limit: 100}
heads, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, query)
require.NoError(t, err)
require.Len(t, heads, 3, "one checked head per runtime and platform, including older runtimes and rollback")
ids := make([]string, 0, len(heads))
for _, head := range heads {
ids = append(ids, head.UpdateId)
if head.UpdateId == "300" {
assert.Equal(t, "Rollback to embedded", head.UpdateUUID)
}
}
assert.ElementsMatch(t, []string{"200", "300", "500"}, ids)
// Applying the cursor after picking each head must never reveal the old
// version of a scope whose newest row appeared on an earlier page.
query.Limit = 1
var pagedIDs []string
for {
page, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, query)
require.NoError(t, err)
if len(page) == 0 {
break
}
require.Len(t, page, 1)
pagedIDs = append(pagedIDs, page[0].UpdateId)
require.LessOrEqual(t, len(pagedIDs), 3, "paging must not repeat a serving head")
query.CursorCreatedAt = &page[0].FeedCreatedAt
query.CursorBranchID = page[0].BranchID
query.CursorUpdateID, err = strconv.ParseInt(page[0].UpdateId, 10, 64)
require.NoError(t, err)
}
assert.ElementsMatch(t, ids, pagedIDs)
filtered, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, types.UpdateFeedQuery{
Branch: rolloutTestDefaultBranch, RuntimeVersion: rolloutTestRuntime,
Platform: types.PlatformIOS, LatestOnly: true, Limit: 100,
})
require.NoError(t, err)
require.Len(t, filtered, 1)
assert.Equal(t, "300", filtered[0].UpdateId)
}
func TestGetLatestUpdateFeedIncludesActiveControlPostgres(t *testing.T) {
fixture := newRolloutFixture(t)
ctx := context.Background()
fixture.checkedUpdate(t, 100, types.PlatformIOS, nil)
fixture.checkedUpdate(t, 200, types.PlatformAndroid, nil)
candidate, err := fixture.updates.CreateUpdateWithRollout(ctx, fixture.appId, 300, rolloutTestDefaultBranch, rolloutTestRuntime, types.PlatformIOS, "candidate", "", 10, nil)
require.NoError(t, err)
require.NoError(t, fixture.updates.MarkUpdateAsChecked(ctx, *candidate))
require.NoError(t, fixture.updates.StoreUpdateUUIDInMetadata(ctx, *candidate, uuid.NewString()))
query := types.UpdateFeedQuery{Branch: rolloutTestDefaultBranch, Platform: types.PlatformIOS, LatestOnly: true, Limit: 100}
active, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, query)
require.NoError(t, err)
require.Len(t, active, 2, "the active rollout serves both the candidate and its control")
assert.ElementsMatch(t, []string{"100", "300"}, []string{active[0].UpdateId, active[1].UpdateId})
_, err = fixture.rollouts.ClearUpdateRollout(ctx, fixture.appId, rolloutTestDefaultBranch, rolloutTestRuntime)
require.NoError(t, err)
finished, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, query)
require.NoError(t, err)
require.Len(t, finished, 1, "a finished rollout no longer serves the historical control")
assert.Equal(t, "300", finished[0].UpdateId)
}
// TestPublishGroupRolloutActivationPostgres verifies the second platform of a
// grouped rollout publish still activates even though the first platform's rollout is already active.
func TestPublishGroupRolloutActivationPostgres(t *testing.T) {
fixture := newRolloutFixture(t)
ctx := context.Background()
group := uuid.NewString()
ios, err := fixture.updates.CreateUpdateWithRollout(ctx, fixture.appId, 600, rolloutTestDefaultBranch, rolloutTestRuntime, "ios", "abc123", "", 10, &group)
require.NoError(t, err)
android, err := fixture.updates.CreateUpdateWithRollout(ctx, fixture.appId, 700, rolloutTestDefaultBranch, rolloutTestRuntime, "android", "abc123", "", 10, &group)
require.NoError(t, err)
require.NoError(t, fixture.updates.MarkUpdateAsChecked(ctx, *ios))
require.NoError(t, fixture.updates.MarkUpdateAsChecked(ctx, *android),
"the second platform of a grouped rollout publish must not be blocked by the first one's active rollout")
active, err := fixture.updates.HasActiveRolloutUpdate(ctx, fixture.appId, rolloutTestDefaultBranch, rolloutTestRuntime)
require.NoError(t, err)
assert.True(t, active)
require.NoError(t, fixture.updates.StoreUpdateUUIDInMetadata(ctx, *ios, uuid.NewString()))
require.NoError(t, fixture.updates.StoreUpdateUUIDInMetadata(ctx, *android, uuid.NewString()))
page, err := fixture.updates.GetUpdatesByRunTimeVersionAndBranchName(ctx, fixture.appId, rolloutTestRuntime, rolloutTestDefaultBranch, nil, 100)
require.NoError(t, err)
items := page.Items
require.Len(t, items, 2)
for _, item := range items {
require.NotNil(t, item.PublishGroup)
assert.Equal(t, group, *item.PublishGroup)
require.NotNil(t, item.RolloutPercentage)
assert.Equal(t, 10, *item.RolloutPercentage)
}
feed, err := fixture.updates.GetUpdateFeed(ctx, fixture.appId, types.UpdateFeedQuery{
PublishGroup: group,
Limit: 10,
})
require.NoError(t, err)
require.Len(t, feed, 2)
for _, item := range feed {
require.NotNil(t, item.RolloutPercentage)
assert.Equal(t, 10, *item.RolloutPercentage)
}
}