diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index 1e547b1a..db19f290 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -119,67 +119,20 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable - /// Migration only!! - /// - public void MigratePhotoSubjects(GamePhoto photo, bool saveChanges) + public GamePhotoSubject AddSubjectForPhoto(GamePhoto photo, int playerId, string displayName, GameUser? user, List bounds, bool save = true) { - List subjects = []; - -#pragma warning disable CS0618 // obsoletion - - // If DisplayName is not null, there is a subject in that spot - if (photo.Subject1DisplayName != null) - { - subjects.Add(new() - { - Photo = photo, - PlayerId = 1, - DisplayName = photo.Subject1DisplayName, - User = photo.Subject1User, - Bounds = photo.Subject1Bounds, - }); - } - - if (photo.Subject2DisplayName != null) - { - subjects.Add(new() - { - Photo = photo, - PlayerId = 2, - DisplayName = photo.Subject2DisplayName, - User = photo.Subject2User, - Bounds = photo.Subject2Bounds, - }); - } - - if (photo.Subject3DisplayName != null) - { - subjects.Add(new() - { - Photo = photo, - PlayerId = 3, - DisplayName = photo.Subject3DisplayName, - User = photo.Subject3User, - Bounds = photo.Subject3Bounds, - }); - } - - if (photo.Subject4DisplayName != null) + GamePhotoSubject subject = new() { - subjects.Add(new() - { - Photo = photo, - PlayerId = 4, - DisplayName = photo.Subject4DisplayName, - User = photo.Subject4User, - Bounds = photo.Subject4Bounds, - }); - } -#pragma warning restore CS0618 + Photo = photo, + PlayerId = playerId, + DisplayName = displayName, + User = user, + Bounds = bounds, + }; - this.GamePhotoSubjects.AddRange(subjects); - if (saveChanges) this.SaveChanges(); + this.GamePhotoSubjects.Add(subject); + if (save) this.SaveChanges(); + return subject; } public void RemovePhoto(GamePhoto photo) @@ -227,6 +180,9 @@ public IQueryable GetSubjectsInPhoto(GamePhoto photo) .Where(s => s.PhotoId == photo.PhotoId) .OrderBy(s => s.PlayerId); + public int GetTotalSubjectsInPhoto(GamePhoto photo) + => this.GamePhotoSubjects.Count(s => s.PhotoId == photo.PhotoId); + public IQueryable GetUsersInPhoto(GamePhoto photo) => this.GetSubjectsInPhoto(photo) .Where(s => s.User != null) diff --git a/Refresh.Database/GameDatabaseContext.Pins.cs b/Refresh.Database/GameDatabaseContext.Pins.cs index d5c6f5c9..7e009d45 100644 --- a/Refresh.Database/GameDatabaseContext.Pins.cs +++ b/Refresh.Database/GameDatabaseContext.Pins.cs @@ -180,8 +180,6 @@ private IEnumerable GetProfilePinsByUser(GameUser user, Toke public DatabaseList GetProfilePinsByUser(GameUser user, TokenGame game, TokenPlatform platform, int skip, int count) => new(this.GetProfilePinsByUser(user, game, platform), skip, count); - - #region Migration Methods public void AddPinProgress(PinProgressRelation relation, bool save) { @@ -189,11 +187,12 @@ public void AddPinProgress(PinProgressRelation relation, bool save) if (save) this.SaveChanges(); } - public void RemoveAllPinProgressesByIdAndUser(long pinId, ObjectId userId, bool save) + public void RemovePinProgress(PinProgressRelation relation, bool save) { - this.PinProgressRelations.RemoveRange(p => p.PinId == pinId && p.PublisherId == userId); + this.PinProgressRelations.Remove(relation); if (save) this.SaveChanges(); } - #endregion + public int GetTotalPinProgresses() + => this.PinProgressRelations.Count(); } \ No newline at end of file diff --git a/Refresh.Database/GameDatabaseContext.Relations.cs b/Refresh.Database/GameDatabaseContext.Relations.cs index 000434a7..16dc718d 100644 --- a/Refresh.Database/GameDatabaseContext.Relations.cs +++ b/Refresh.Database/GameDatabaseContext.Relations.cs @@ -477,20 +477,6 @@ public GameReview UpdateReview(ISubmitReviewRequest updateInfo, GameReview revie this.SaveChanges(); return review; } - - public void MigrateReviewLabels(IEnumerable reviews) - { - foreach (GameReview review in reviews) - { -#pragma warning disable CS0618 // LabelsString is obsolete - if (string.IsNullOrWhiteSpace(review.LabelsString)) continue; - - review.Labels = LabelExtensions.FromLbpCommaList(review.LabelsString).ToList(); -#pragma warning restore CS0618 - } - - this.SaveChanges(); - } public void DeleteReviewsPostedByUser(GameUser user) { diff --git a/Refresh.Interfaces.Workers/Migrations/BackfillLevelAttributesMigration.cs b/Refresh.Interfaces.Workers/Migrations/BackfillLevelAttributesMigration.cs index 57299f41..cb769df5 100644 --- a/Refresh.Interfaces.Workers/Migrations/BackfillLevelAttributesMigration.cs +++ b/Refresh.Interfaces.Workers/Migrations/BackfillLevelAttributesMigration.cs @@ -12,7 +12,7 @@ protected override IQueryable SortAndFilter(IQueryable que .OrderBy(l => l.LevelId); } - protected override void Migrate(WorkContext context, GameLevel[] batch) + protected override int Migrate(WorkContext context, GameLevel[] batch) { foreach (GameLevel level in batch) { @@ -20,5 +20,6 @@ protected override void Migrate(WorkContext context, GameLevel[] batch) } context.Database.SaveChanges(); + return batch.Length; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/Migrations/BackfillModdedPlanetFlagsMigration.cs b/Refresh.Interfaces.Workers/Migrations/BackfillModdedPlanetFlagsMigration.cs index cb95d8d1..47664158 100644 --- a/Refresh.Interfaces.Workers/Migrations/BackfillModdedPlanetFlagsMigration.cs +++ b/Refresh.Interfaces.Workers/Migrations/BackfillModdedPlanetFlagsMigration.cs @@ -16,7 +16,7 @@ protected override IQueryable SortAndFilter(IQueryable query || u.BetaPlanetsHash != "0"); } - protected override void Migrate(WorkContext context, GameUser[] batch) + protected override int Migrate(WorkContext context, GameUser[] batch) { foreach (GameUser user in batch) { @@ -24,5 +24,6 @@ protected override void Migrate(WorkContext context, GameUser[] batch) } context.Database.SaveChanges(); + return batch.Length; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/Migrations/BackfillReviewLabelsMigration.cs b/Refresh.Interfaces.Workers/Migrations/BackfillReviewLabelsMigration.cs index 2a2a69ff..1c976af9 100644 --- a/Refresh.Interfaces.Workers/Migrations/BackfillReviewLabelsMigration.cs +++ b/Refresh.Interfaces.Workers/Migrations/BackfillReviewLabelsMigration.cs @@ -1,13 +1,25 @@ -using Refresh.Database.Models.Comments; +using Refresh.Database.Models.Comments; +using Refresh.Database.Models.Levels; using Refresh.Workers; namespace Refresh.Interfaces.Workers.Migrations; public class BackfillReviewLabelsMigration : MigrationJob { - protected override void Migrate(WorkContext context, GameReview[] batch) + protected override int Migrate(WorkContext context, GameReview[] batch) { - context.Database.MigrateReviewLabels(batch); + foreach (GameReview review in batch) + { +#pragma warning disable CS0618 // LabelsString is obsolete + if (string.IsNullOrWhiteSpace(review.LabelsString)) continue; + + context.Database.Update(review); + review.Labels = LabelExtensions.FromLbpCommaList(review.LabelsString).ToList(); +#pragma warning restore CS0618 + } + + context.Database.SaveChanges(); + return batch.Length; } protected override IQueryable SortAndFilter(IQueryable query) diff --git a/Refresh.Interfaces.Workers/Migrations/BackfillRevisionMigration.cs b/Refresh.Interfaces.Workers/Migrations/BackfillRevisionMigration.cs index 5a8b3d4f..ba97a96c 100644 --- a/Refresh.Interfaces.Workers/Migrations/BackfillRevisionMigration.cs +++ b/Refresh.Interfaces.Workers/Migrations/BackfillRevisionMigration.cs @@ -5,12 +5,13 @@ namespace Refresh.Interfaces.Workers.Migrations; public class BackfillRevisionMigration : MigrationJob { - protected override void Migrate(WorkContext context, GameLevel[] batch) + protected override int Migrate(WorkContext context, GameLevel[] batch) { foreach (GameLevel level in batch) { context.Database.CreateRevisionForLevel(level, null); } + return batch.Length; } protected override IQueryable SortAndFilter(IQueryable query) diff --git a/Refresh.Interfaces.Workers/Migrations/CalculateScoreRanksMigration.cs b/Refresh.Interfaces.Workers/Migrations/CalculateScoreRanksMigration.cs index 60419ee4..04b12a65 100644 --- a/Refresh.Interfaces.Workers/Migrations/CalculateScoreRanksMigration.cs +++ b/Refresh.Interfaces.Workers/Migrations/CalculateScoreRanksMigration.cs @@ -12,12 +12,13 @@ protected override IQueryable SortAndFilter(IQueryable que return query.OrderBy(l => l.LevelId); } - protected override void Migrate(WorkContext context, GameLevel[] batch) + protected override int Migrate(WorkContext context, GameLevel[] batch) { foreach (GameLevel level in batch) { context.Database.RecalculateScoreStatistics(level); } context.Database.SaveChanges(); + return batch.Length; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/Migrations/ClampPlayerLimitsMigration.cs b/Refresh.Interfaces.Workers/Migrations/ClampPlayerLimitsMigration.cs index feb9fc70..d3c47f32 100644 --- a/Refresh.Interfaces.Workers/Migrations/ClampPlayerLimitsMigration.cs +++ b/Refresh.Interfaces.Workers/Migrations/ClampPlayerLimitsMigration.cs @@ -13,7 +13,7 @@ protected override IQueryable SortAndFilter(IQueryable que return query.OrderBy(l => l.LevelId); } - protected override void Migrate(WorkContext context, GameLevel[] batch) + protected override int Migrate(WorkContext context, GameLevel[] batch) { foreach (GameLevel level in batch) { @@ -26,5 +26,7 @@ protected override void Migrate(WorkContext context, GameLevel[] batch) level.MinPlayers = Math.Clamp(level.MinPlayers, 1, 4); level.MaxPlayers = Math.Clamp(level.MaxPlayers, 1, 4); } + + return batch.Length; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/Migrations/CorrectWebsitePinProgressPlatform.cs b/Refresh.Interfaces.Workers/Migrations/CorrectWebsitePinProgressPlatformMigration.cs similarity index 67% rename from Refresh.Interfaces.Workers/Migrations/CorrectWebsitePinProgressPlatform.cs rename to Refresh.Interfaces.Workers/Migrations/CorrectWebsitePinProgressPlatformMigration.cs index 18a2ccb3..45783ed6 100644 --- a/Refresh.Interfaces.Workers/Migrations/CorrectWebsitePinProgressPlatform.cs +++ b/Refresh.Interfaces.Workers/Migrations/CorrectWebsitePinProgressPlatformMigration.cs @@ -7,9 +7,9 @@ namespace Refresh.Interfaces.Workers.Migrations; -public class CorrectWebsitePinProgressPlatform : MigrationJob +public class CorrectWebsitePinProgressPlatformMigration : MigrationJob { - private List websitePinIds = + private readonly List WebsitePinIds = [ (long)ServerPins.HeartPlayerOnWebsite, (long)ServerPins.QueueLevelOnWebsite, @@ -19,13 +19,15 @@ public class CorrectWebsitePinProgressPlatform : MigrationJob SortAndFilter(IQueryable query) { return query - .Where(p => this.websitePinIds.Contains(p.PinId)) + .Where(p => this.WebsitePinIds.Contains(p.PinId)) .OrderBy(p => p.PinId); } - protected override void Migrate(WorkContext context, PinProgressRelation[] batch) + protected override int Migrate(WorkContext context, PinProgressRelation[] batch) { - foreach (long pinId in this.websitePinIds) + int pinsLeft = batch.Length; + + foreach (long pinId in this.WebsitePinIds) { IEnumerable> pinsByUser = batch .Where(r => r.PinId == pinId) @@ -38,12 +40,16 @@ protected override void Migrate(WorkContext context, PinProgressRelation[] batch // Find best one by the current user PinProgressRelation relationToMigrate = group.MaxBy(r => r.Progress)!; + List relationsToRemove = group.ToList(); - // Remove all already existing progresses to remove duplicates - context.Database.RemoveAllPinProgressesByIdAndUser(pinId, relationToMigrate.PublisherId, false); + foreach (PinProgressRelation relation in group) + { + context.Database.RemovePinProgress(relation, false); + pinsLeft--; + } // Now take the best progress we've just got and add it as a website pin, preserving other old metadata - context.Database.AddPinProgress(new() + PinProgressRelation newRelation = new() { PinId = relationToMigrate.PinId, Progress = relationToMigrate.Progress, @@ -52,10 +58,14 @@ protected override void Migrate(WorkContext context, PinProgressRelation[] batch LastUpdated = relationToMigrate.LastUpdated, IsBeta = false, // doesn't matter here Platform = TokenPlatform.Website, - }, false); + }; + + context.Database.AddPinProgress(newRelation, false); + pinsLeft++; } } context.Database.SaveChanges(); + return pinsLeft; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/Migrations/EnsureDeletedUsersDeletedMigration.cs b/Refresh.Interfaces.Workers/Migrations/EnsureDeletedUsersDeletedMigration.cs index a1c8380d..52f2e4ae 100644 --- a/Refresh.Interfaces.Workers/Migrations/EnsureDeletedUsersDeletedMigration.cs +++ b/Refresh.Interfaces.Workers/Migrations/EnsureDeletedUsersDeletedMigration.cs @@ -15,12 +15,13 @@ protected override IQueryable SortAndFilter(IQueryable query .OrderBy(u => u.UserId); // can't use join date here as we're changing the join date when we delete data } - protected override void Migrate(WorkContext context, GameUser[] batch) + protected override int Migrate(WorkContext context, GameUser[] batch) { foreach (GameUser user in batch) { context.Logger.LogWarning(RefreshContext.Worker, $"Deleting {user.Username}'s account again to ensure data has been wiped..."); context.Database.DeleteUser(user); } + return batch.Length; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs b/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs index d895199d..7c4b1fdc 100644 --- a/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs +++ b/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs @@ -7,16 +7,46 @@ public class MoveSubjectsOutOfGamePhotosMigration : MigrationJob { protected override IQueryable SortAndFilter(IQueryable query) { - return query.OrderBy(p => p.PhotoId); + return query + .Where(p => p.Subjects.Count == 0) + .OrderBy(p => p.PhotoId); } - protected override void Migrate(WorkContext context, GamePhoto[] batch) + protected override int Migrate(WorkContext context, GamePhoto[] batch) { foreach (GamePhoto photo in batch) { - context.Database.MigratePhotoSubjects(photo, false); + // Extra check to be sure + if (context.Database.GetTotalSubjectsInPhoto(photo) > 0) + continue; + + #pragma warning disable CS0618 // obsoletion + + // If DisplayName is not null, there is a subject in that spot + if (photo.Subject1DisplayName != null) + { + context.Database.AddSubjectForPhoto(photo, 1, photo.Subject1DisplayName, photo.Subject1User, photo.Subject1Bounds, false); + } + + if (photo.Subject2DisplayName != null) + { + context.Database.AddSubjectForPhoto(photo, 2, photo.Subject2DisplayName, photo.Subject2User, photo.Subject2Bounds, false); + } + + if (photo.Subject3DisplayName != null) + { + context.Database.AddSubjectForPhoto(photo, 3, photo.Subject3DisplayName, photo.Subject3User, photo.Subject3Bounds, false); + } + + if (photo.Subject4DisplayName != null) + { + context.Database.AddSubjectForPhoto(photo, 4, photo.Subject4DisplayName, photo.Subject4User, photo.Subject4Bounds, false); + } + + #pragma warning restore CS0618 } context.Database.SaveChanges(); + return batch.Length; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs index b5307090..428363d9 100644 --- a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs +++ b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs @@ -25,7 +25,7 @@ public static WorkerManager Create(Logger logger, IDataStore dataStore, GameData manager.AddJob(); manager.AddJob(); manager.AddJob(); - manager.AddJob(); + manager.AddJob(); manager.AddJob(); manager.AddJob(); diff --git a/Refresh.Workers/MigrationJob.cs b/Refresh.Workers/MigrationJob.cs index 3e9e66ed..0546156a 100644 --- a/Refresh.Workers/MigrationJob.cs +++ b/Refresh.Workers/MigrationJob.cs @@ -40,15 +40,23 @@ public override void ExecuteJob(WorkContext context) TEntity[] batch = query.ToArray(); - Migrate(context, batch); + int entitiesLeftCount = Migrate(context, batch); context.Database.SaveChanges(); transaction.Commit(); - state.Processed += batch.Length; + state.Processed += entitiesLeftCount; + state.Total = state.Total - batch.Length + entitiesLeftCount; context.Logger.LogInfo(RefreshContext.Database, $"{this.JobId} migrated {batch.Length} objects ({state.Processed}/{state.Total}, complete: {state.Complete})"); + + context.Database.UpdateOrCreateJobState(this.GetType().Name, state, this.JobClass); } protected abstract IQueryable SortAndFilter(IQueryable query); - protected abstract void Migrate(WorkContext context, TEntity[] batch); + /// + /// The number of entities in the batch, minus the number of entities removed from DB during this migration. + /// E.g. if 1000 items are in the given batch, and 5 got removed from DB during migration, + /// the returned number will be 1000 - 5 = 995 + /// + protected abstract int Migrate(WorkContext context, TEntity[] batch); } \ No newline at end of file diff --git a/RefreshTests.GameServer/GameServer/TestMigrationJob.cs b/RefreshTests.GameServer/GameServer/TestMigrationJob.cs index 015dc316..715427ee 100644 --- a/RefreshTests.GameServer/GameServer/TestMigrationJob.cs +++ b/RefreshTests.GameServer/GameServer/TestMigrationJob.cs @@ -5,12 +5,14 @@ namespace RefreshTests.GameServer.GameServer; public class TestMigrationJob : MigrationJob { - protected override void Migrate(WorkContext context, GameLevel[] batch) + protected override int Migrate(WorkContext context, GameLevel[] batch) { foreach (GameLevel level in batch) { level.Title += " test"; } + + return batch.Length; } protected override IQueryable SortAndFilter(IQueryable query) diff --git a/RefreshTests.GameServer/GameServer/TestMigrationJobDeletingLevels.cs b/RefreshTests.GameServer/GameServer/TestMigrationJobDeletingLevels.cs new file mode 100644 index 00000000..5a059fae --- /dev/null +++ b/RefreshTests.GameServer/GameServer/TestMigrationJobDeletingLevels.cs @@ -0,0 +1,37 @@ +using Refresh.Database.Models.Levels; +using Refresh.Workers; + +namespace RefreshTests.GameServer.GameServer; + +public class TestMigrationJobDeletingLevels : MigrationJob +{ + protected override int BatchCount => 6; + + // Deletes every second level + protected override int Migrate(WorkContext context, GameLevel[] batch) + { + int entitiesLeft = batch.Length; + int index = 0; + foreach (GameLevel level in batch) + { + if (index % 2 == 1) + { + context.Database.DeleteLevel(level); + entitiesLeft--; + } + else + { + level.Title += " test"; + } + + index++; + } + + return entitiesLeft; + } + + protected override IQueryable SortAndFilter(IQueryable query) + { + return query.OrderBy(l => l.LevelId); + } +} \ No newline at end of file diff --git a/RefreshTests.GameServer/Tests/Workers/MigrationTests.cs b/RefreshTests.GameServer/Tests/Workers/MigrationTests.cs index 44193777..d3a8caf2 100644 --- a/RefreshTests.GameServer/Tests/Workers/MigrationTests.cs +++ b/RefreshTests.GameServer/Tests/Workers/MigrationTests.cs @@ -1,7 +1,7 @@ using Refresh.Database.Models.Authentication; using Refresh.Database.Models.Levels; using Refresh.Database.Models.Users; -using Refresh.Database.Query; +using Refresh.Database.Models.Workers; using Refresh.Workers.State; namespace RefreshTests.GameServer.Tests.Workers; @@ -29,4 +29,79 @@ public void MigrationJobWorks() allLevels = context.Database.GetNewestLevels(100, 0, null, new(TokenGame.Website)).Items; Assert.That(allLevels.All(l => l.Title == "Level test"), Is.True); } + + [Test] + public void DeletingEntitiesDuringMigrationJobDoesNotBreakPagination() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + // 3 cycles (6 levels per cycle) + for (int i = 0; i < 18; i++) + { + context.CreateLevel(user); + } + context.Database.Refresh(); + Assert.That(context.Database.GetTotalLevelCount(), Is.EqualTo(18)); + + // Prepare migration + TestMigrationJobDeletingLevels job = new(); + context.Database.UpdateOrCreateJobState(typeof(TestMigrationJobDeletingLevels).Name, new MigrationJobState() + { + Total = 18 + }, WorkerClass.Refresh); + context.Database.Refresh(); + + object? stateObject = context.Database.GetJobState(typeof(TestMigrationJobDeletingLevels).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + job.JobState = stateObject!; + + // Migrate - First cycle + job.ExecuteJob(context.GetWorkContext()); + context.Database.Refresh(); + + // Ensure only 3 levels were actually migrated, and 3 levels were deleted + Assert.That(context.Database.GetTotalLevelCount(), Is.EqualTo(15)); + Assert.That(context.Database.GetNewestLevels(20, 0, null, new(TokenGame.Website)).Items.Count(l => l.Title.EndsWith(" test")), Is.EqualTo(3)); + + stateObject = context.Database.GetJobState(typeof(TestMigrationJobDeletingLevels).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + + MigrationJobState jobState = (MigrationJobState)stateObject!; + Assert.That(jobState.Processed, Is.EqualTo(3)); + Assert.That(jobState.Total, Is.EqualTo(15)); + Assert.That(jobState.Complete, Is.False); + + // Migrate - Second cycle + job.ExecuteJob(context.GetWorkContext()); + context.Database.Refresh(); + + // Ensure 3 more levels were deleted, and 3 more levels were migrated + Assert.That(context.Database.GetTotalLevelCount(), Is.EqualTo(12)); + Assert.That(context.Database.GetNewestLevels(20, 0, null, new(TokenGame.Website)).Items.Count(l => l.Title.EndsWith(" test")), Is.EqualTo(6)); + + stateObject = context.Database.GetJobState(typeof(TestMigrationJobDeletingLevels).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + + jobState = (MigrationJobState)stateObject!; + Assert.That(jobState.Processed, Is.EqualTo(6)); + Assert.That(jobState.Total, Is.EqualTo(12)); + Assert.That(jobState.Complete, Is.False); + + // Migrate - Last cycle + job.ExecuteJob(context.GetWorkContext()); + context.Database.Refresh(); + + // Ensure 3 more levels were deleted, and 3 more levels were migrated + Assert.That(context.Database.GetTotalLevelCount(), Is.EqualTo(9)); + Assert.That(context.Database.GetNewestLevels(20, 0, null, new(TokenGame.Website)).Items.Count(l => l.Title.EndsWith(" test")), Is.EqualTo(9)); + + stateObject = context.Database.GetJobState(typeof(TestMigrationJobDeletingLevels).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + + jobState = (MigrationJobState)stateObject!; + Assert.That(jobState.Processed, Is.EqualTo(9)); + Assert.That(jobState.Total, Is.EqualTo(9)); + Assert.That(jobState.Complete, Is.True); + } } \ No newline at end of file diff --git a/RefreshTests.GameServer/Tests/Workers/PhotoMigrationTests.cs b/RefreshTests.GameServer/Tests/Workers/PhotoMigrationTests.cs new file mode 100644 index 00000000..6876ee19 --- /dev/null +++ b/RefreshTests.GameServer/Tests/Workers/PhotoMigrationTests.cs @@ -0,0 +1,173 @@ +using Refresh.Database.Models.Assets; +using Refresh.Database.Models.Photos; +using Refresh.Database.Models.Users; +using Refresh.Database.Models.Workers; +using Refresh.Interfaces.Workers.Migrations; +using Refresh.Workers.State; + +namespace RefreshTests.GameServer.Tests.Workers; + +public class PhotoMigrationTests : GameServerTest +{ + private const string TEST_ASSET_HASH = "0ec63b140374ba704a58fa0c743cb357683313dd"; + + [Test] + public void MigratesPhotoSubjectsButNotMultipleTimes() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + context.Database.AddAssetToDatabase(new() + { + AssetHash = TEST_ASSET_HASH, + AssetType = GameAssetType.Png, + AssetFormat = GameAssetFormat.Unknown, + OriginalUploader = user, + UploadDate = new(new DateTime(2026, 3, 11), TimeSpan.Zero), + }); + + for (int i = 0; i < 10; i++) + { + context.Database.Add(new GamePhoto() + { + TakenAt = new(new DateTime(2026, 3, 18), TimeSpan.Zero), + PublishedAt = new(new DateTime(2026, 3, 18), TimeSpan.Zero), + PublisherId = user.UserId, + SmallAssetHash = TEST_ASSET_HASH, + MediumAssetHash = TEST_ASSET_HASH, + LargeAssetHash = TEST_ASSET_HASH, + PlanHash = "plan", + LevelType = "pod", + OriginalLevelId = 0, + OriginalLevelName = "", + + #pragma warning disable CS0618 // obsoletion + Subject1DisplayName = "p", + Subject1Bounds = [3,1,4,1], + Subject2DisplayName = "i", + Subject2Bounds = [5,9,2,7], + #pragma warning restore CS0618 + }); + } + + context.Database.SaveChanges(); + Assert.That(context.Database.GetTotalPhotoCount(), Is.EqualTo(10)); + + MoveSubjectsOutOfGamePhotosMigration job = new(); + context.Database.UpdateOrCreateJobState(typeof(MoveSubjectsOutOfGamePhotosMigration).Name, new MigrationJobState() + { + Total = 10 + }, WorkerClass.Refresh); + context.Database.Refresh(); + + object? stateObject = context.Database.GetJobState(typeof(MoveSubjectsOutOfGamePhotosMigration).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + job.JobState = stateObject!; + + // Migrate - First cycle + job.ExecuteJob(context.GetWorkContext()); + context.Database.Refresh(); + + Assert.That(context.Database.GetTotalPhotoCount(), Is.EqualTo(10)); + foreach(GamePhoto photo in context.Database.GetRecentPhotos(10, 0).Items.ToArray()) + { + Assert.That(photo.Subjects.Count, Is.EqualTo(2)); + Assert.That(context.Database.GetTotalSubjectsInPhoto(photo), Is.EqualTo(2)); + + Assert.That(photo.Subjects.Count(s => s.DisplayName == "p"), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.DisplayName == "i"), Is.EqualTo(1)); + + Assert.That(photo.Subjects.Count(s => s.PlayerId == 1), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.PlayerId == 2), Is.EqualTo(1)); + } + + stateObject = context.Database.GetJobState(typeof(MoveSubjectsOutOfGamePhotosMigration).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + + MigrationJobState jobState = (MigrationJobState)stateObject!; + Assert.That(jobState.Processed, Is.EqualTo(10)); + Assert.That(jobState.Total, Is.EqualTo(10)); + Assert.That(jobState.Complete, Is.True); + + // Add new, unmigrated photos + for (int i = 0; i < 5; i++) + { + context.Database.Add(new GamePhoto() + { + TakenAt = new(new DateTime(2026, 3, 27), TimeSpan.Zero), + PublishedAt = new(new DateTime(2026, 3, 27), TimeSpan.Zero), + PublisherId = user.UserId, + SmallAssetHash = TEST_ASSET_HASH, + MediumAssetHash = TEST_ASSET_HASH, + LargeAssetHash = TEST_ASSET_HASH, + PlanHash = "plan", + LevelType = "pod", + OriginalLevelId = 0, + OriginalLevelName = "", + + #pragma warning disable CS0618 // obsoletion + Subject1DisplayName = "d", + Subject1Bounds = [1,2,3,4], + Subject2DisplayName = "a", + Subject2Bounds = [1,2,3,4], + Subject3DisplayName = "n", + Subject3Bounds = [1,2,3,4], + Subject4DisplayName = "k", + Subject4Bounds = [1,2,3,4], + #pragma warning restore CS0618 + }); + } + context.Database.SaveChanges(); + Assert.That(context.Database.GetTotalPhotoCount(), Is.EqualTo(15)); + context.Database.Refresh(); + + // Reset state, to test both the migrated photos not having their subjects re-migrated (should be skipped entirely due to SortAndFilter()), + // and to also test the new photos being migrated + jobState.Total = 5; + jobState.Processed = 0; + job.JobState = jobState; + context.Database.UpdateOrCreateJobState(typeof(MoveSubjectsOutOfGamePhotosMigration).Name, jobState, WorkerClass.Refresh); + context.Database.Refresh(); + + // Migrate - Second cycle + job.ExecuteJob(context.GetWorkContext()); + context.Database.Refresh(); + + Assert.That(context.Database.GetTotalPhotoCount(), Is.EqualTo(15)); + foreach(GamePhoto photo in context.Database.GetRecentPhotos(10, 5).Items.ToArray()) + { + // The photos from the first cycle still only have 2 subjects each + Assert.That(photo.Subjects.Count, Is.EqualTo(2)); + Assert.That(context.Database.GetTotalSubjectsInPhoto(photo), Is.EqualTo(2)); + + Assert.That(photo.Subjects.Count(s => s.DisplayName == "p"), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.DisplayName == "i"), Is.EqualTo(1)); + + Assert.That(photo.Subjects.Count(s => s.PlayerId == 1), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.PlayerId == 2), Is.EqualTo(1)); + } + + foreach(GamePhoto photo in context.Database.GetRecentPhotos(5, 0).Items.ToArray()) + { + Assert.That(photo.Subjects.Count, Is.EqualTo(4)); + Assert.That(context.Database.GetTotalSubjectsInPhoto(photo), Is.EqualTo(4)); + + Assert.That(photo.Subjects.Count(s => s.DisplayName == "d"), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.DisplayName == "a"), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.DisplayName == "n"), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.DisplayName == "k"), Is.EqualTo(1)); + + Assert.That(photo.Subjects.Count(s => s.PlayerId == 1), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.PlayerId == 2), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.PlayerId == 3), Is.EqualTo(1)); + Assert.That(photo.Subjects.Count(s => s.PlayerId == 4), Is.EqualTo(1)); + } + + stateObject = context.Database.GetJobState(typeof(MoveSubjectsOutOfGamePhotosMigration).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + + jobState = (MigrationJobState)stateObject!; + Assert.That(jobState.Processed, Is.EqualTo(5)); + Assert.That(jobState.Total, Is.EqualTo(5)); + Assert.That(jobState.Complete, Is.True); + } +} \ No newline at end of file diff --git a/RefreshTests.GameServer/Tests/Workers/PinMigrationTests.cs b/RefreshTests.GameServer/Tests/Workers/PinMigrationTests.cs new file mode 100644 index 00000000..bf011ea4 --- /dev/null +++ b/RefreshTests.GameServer/Tests/Workers/PinMigrationTests.cs @@ -0,0 +1,73 @@ +using Refresh.Database.Models.Authentication; +using Refresh.Database.Models.Pins; +using Refresh.Database.Models.Users; +using Refresh.Database.Models.Workers; +using Refresh.Interfaces.Workers.Migrations; +using Refresh.Workers.State; + +namespace RefreshTests.GameServer.Tests.Workers; + +public class PinMigrationTests : GameServerTest +{ + [Test] + public void WebsitePinMigrationDoesNotBreakPagination() + { + using TestContext context = this.GetServer(); + + for (int i = 0; i < 50; i++) + { + GameUser user = context.CreateUser(); + context.Database.AddPinProgress(new() + { + PinId = (long)ServerPins.SignIntoWebsite, + Progress = i + 1, + Publisher = user, + FirstPublished = new(), + LastUpdated = new(), + IsBeta = false, + Platform = TokenPlatform.RPCS3, + }, false); + + context.Database.AddPinProgress(new() + { + PinId = (long)ServerPins.SignIntoWebsite, + Progress = i + 1, + Publisher = user, + FirstPublished = new(), + LastUpdated = new(), + IsBeta = true, + Platform = TokenPlatform.RPCS3, + }, false); + } + context.Database.SaveChanges(); + Assert.That(context.Database.GetTotalPinProgresses(), Is.EqualTo(100)); + + // Prepare migration + CorrectWebsitePinProgressPlatformMigration job = new(); + context.Database.UpdateOrCreateJobState(typeof(CorrectWebsitePinProgressPlatformMigration).Name, new MigrationJobState() + { + Total = 100 // Since we already have to manually create the state + }, WorkerClass.Refresh); + context.Database.Refresh(); + + object? stateObject = context.Database.GetJobState(typeof(CorrectWebsitePinProgressPlatformMigration).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + job.JobState = stateObject!; + + // Migrate + job.ExecuteJob(context.GetWorkContext()); + context.Database.Refresh(); + + stateObject = context.Database.GetJobState(typeof(CorrectWebsitePinProgressPlatformMigration).Name, typeof(MigrationJobState), WorkerClass.Refresh); + Assert.That(stateObject, Is.Not.Null); + + // While we're not actually testing pagination here (batch count can't be edited + don't want to create over 1000 users here, would take too long), + // it should be enough to simply ensure that both the total and processed counts are adjusted to be less than 100 (as 50 pins were deleted) + MigrationJobState jobState = (MigrationJobState)stateObject!; + Assert.That(jobState.Processed, Is.EqualTo(50)); + Assert.That(jobState.Total, Is.EqualTo(50)); + Assert.That(jobState.Complete, Is.True); + + Assert.That(context.Database.GetTotalPinProgresses(), Is.EqualTo(50)); + } +} \ No newline at end of file