From 3af23058a0b28203b1e229d433a15a2e695d4be2 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 9 Mar 2026 12:06:07 +0100 Subject: [PATCH 01/11] Split GamePhotoSubjects into own DB table --- .../GameDatabaseContext.Photos.cs | 120 +++++++++++------- Refresh.Database/GameDatabaseContext.Users.cs | 10 +- Refresh.Database/GameDatabaseContext.cs | 1 + Refresh.Database/Models/Photos/GamePhoto.cs | 88 +------------ .../Models/Photos/GamePhotoSubject.cs | 29 ++--- .../Users/Photos/ApiGamePhotoResponse.cs | 2 +- 6 files changed, 97 insertions(+), 153 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index 9535abc76..f749e1306 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -5,28 +5,30 @@ using Refresh.Database.Models.Photos; using Refresh.Database.Query; using Refresh.Database.Helpers; +using Bunkum.Core; namespace Refresh.Database; public partial class GameDatabaseContext // Photos { private IQueryable GamePhotosIncluded => this.GamePhotos - .Include(p => p.LargeAsset) - .Include(p => p.MediumAsset) - .Include(p => p.SmallAsset) .Include(p => p.Publisher) .Include(p => p.Publisher.Statistics) .Include(p => p.Level) .Include(p => p.Level!.Publisher) - .Include(p => p.Level!.Publisher!.Statistics) - .Include(p => p.Subject1User) - .Include(p => p.Subject1User!.Statistics) - .Include(p => p.Subject2User) - .Include(p => p.Subject2User!.Statistics) - .Include(p => p.Subject3User) - .Include(p => p.Subject3User!.Statistics) - .Include(p => p.Subject4User) - .Include(p => p.Subject4User!.Statistics); + .Include(p => p.Level!.Publisher!.Statistics); + + private IQueryable GamePhotoSubjectsIncludingUsers => this.GamePhotoSubjects + .Include(p => p.User) + .Include(p => p.User!.Statistics); + + private IQueryable GamePhotoSubjectsIncludingPhotos => this.GamePhotoSubjects + .Include(p => p.Photo) + .Include(p => p.Photo!.Publisher) + .Include(p => p.Photo!.Publisher.Statistics) + .Include(p => p.Photo!.Level) + .Include(p => p.Photo!.Level!.Publisher) + .Include(p => p.Photo!.Level!.Publisher!.Statistics); public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable subjects, GameUser publisher, GameLevel? level) { @@ -47,29 +49,6 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable gameSubjects = new(subjects.Count()); - foreach (IPhotoUploadSubject subject in subjects) - { - GameUser? subjectUser = null; - - if (!string.IsNullOrEmpty(subject.Username)) - subjectUser = this.GetUserByUsername(subject.Username); - - float[] bounds = PhotoHelper.ParseBoundsList(subject.BoundsList); - - gameSubjects.Add(new GamePhotoSubject(subjectUser, subject.DisplayName, bounds)); - - if (subjectUser != null) - { - this.WriteEnsuringStatistics(subjectUser, () => - { - subjectUser.Statistics!.PhotosWithUserCount++; - }); - } - } - - newPhoto.Subjects = gameSubjects; - this.WriteEnsuringStatistics(publisher, () => { this.GamePhotos.Add(newPhoto); @@ -87,6 +66,46 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable subjectsList = subjects.ToList(); + List finalSubjectsList = []; + + // Take care of subjects after saving the photo itself to keep the photo, even if its subjects are malformed + for (int i = 0; i < subjectsList.Count; i++) + { + IPhotoUploadSubject subject = subjectsList[i]; + + float[] bounds = new float[PhotoHelper.SubjectBoundaryCount]; + try + { + bounds = PhotoHelper.ParseBoundsList(subject.BoundsList); + } + catch (Exception ex) + { + this._logger.LogWarning(BunkumCategory.UserPhotos, $"Could not parse {subject.DisplayName}'s photo bounds: {ex.GetType()} {ex.Message}"); + } + + GameUser? subjectUser = string.IsNullOrWhiteSpace(subject.Username) ? null : this.GetUserByUsername(subject.Username); + finalSubjectsList.Add(new() + { + Photo = newPhoto, + User = subjectUser, + DisplayName = subject.DisplayName, + PlayerId = i, + Bounds = bounds + }); + + if (subjectUser != null) + { + this.WriteEnsuringStatistics(subjectUser, () => + { + subjectUser.Statistics!.PhotosWithUserCount++; + }); + } + } + + this.GamePhotoSubjects.AddRange(finalSubjectsList); + this.SaveChanges(); this.CreatePhotoUploadEvent(publisher, newPhoto); return newPhoto; @@ -94,15 +113,12 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable { - this.WriteEnsuringStatistics(subject.User, () => - { - subject.User.Statistics!.PhotosWithUserCount--; - }); - } + subjectUser.Statistics!.PhotosWithUserCount--; + }); } if (photo.Level != null) @@ -124,6 +140,9 @@ public void RemovePhoto(GamePhoto photo) // Remove all events referencing the photo this.Events.RemoveRange(photoEvents); + + // Remove all subjects + this.GamePhotoSubjects.RemoveRange(s => s.PhotoId == photo.PhotoId); // Remove the photo this.GamePhotos.Remove(photo); @@ -132,6 +151,14 @@ public void RemovePhoto(GamePhoto photo) }); } + public IQueryable GetSubjectsInPhoto(GamePhoto photo) + => this.GamePhotoSubjectsIncludingUsers.Where(s => s.PhotoId == photo.PhotoId); + + public IQueryable GetUsersInPhoto(GamePhoto photo) + => this.GetSubjectsInPhoto(photo) + .Where(s => s.User != null) + .Select(s => s.User!); + public int GetTotalPhotoCount() => this.GamePhotos.Count(); [Pure] @@ -155,14 +182,13 @@ public int GetTotalPhotosByUser(GameUser user) [Pure] public DatabaseList GetPhotosWithUser(GameUser user, int count, int skip) => - new(this.GamePhotosIncluded - .Where(p => p.Subject1UserId == user.UserId || p.Subject2UserId == user.UserId || p.Subject3UserId == user.UserId || p.Subject4UserId == user.UserId) - .OrderByDescending(p => p.TakenAt), skip, count); + new(this.GamePhotoSubjectsIncludingPhotos + .Where(s => s.UserId == user.UserId) + .Select(s => s.Photo), skip, count); [Pure] public int GetTotalPhotosWithUser(GameUser user) - => this.GamePhotos - .Count(p => p.Subject1UserId == user.UserId || p.Subject2UserId == user.UserId || p.Subject3UserId == user.UserId || p.Subject4UserId == user.UserId); + => this.GamePhotoSubjects.Count(s => s.UserId == user.UserId); [Pure] public DatabaseList GetPhotosInLevel(GameLevel level, int count, int skip) diff --git a/Refresh.Database/GameDatabaseContext.Users.cs b/Refresh.Database/GameDatabaseContext.Users.cs index 2ec99d8b6..25b426724 100644 --- a/Refresh.Database/GameDatabaseContext.Users.cs +++ b/Refresh.Database/GameDatabaseContext.Users.cs @@ -401,14 +401,16 @@ public void DeleteUser(GameUser user) user.PsnAuthenticationAllowed = false; user.RpcnAuthenticationAllowed = false; - foreach (GamePhoto photo in this.GetPhotosWithUser(user, int.MaxValue, 0).Items) - foreach (GamePhotoSubject subject in photo.Subjects.Where(s => s.User?.UserId == user.UserId)) - subject.User = null; - + foreach (GamePhotoSubject subject in this.GamePhotoSubjects.Where(s => s.UserId == user.UserId)) + { + subject.UserId = null; + } + this.FavouriteLevelRelations.RemoveRange(r => r.User == user); this.FavouriteUserRelations.RemoveRange(r => r.UserToFavourite == user); this.FavouriteUserRelations.RemoveRange(r => r.UserFavouriting == user); this.QueueLevelRelations.RemoveRange(r => r.User == user); + this.GamePhotoSubjects.RemoveRange(s => s.Photo.PublisherId == user.UserId); this.GamePhotos.RemoveRange(p => p.Publisher == user); this.GameUserVerifiedIpRelations.RemoveRange(p => p.User == user); diff --git a/Refresh.Database/GameDatabaseContext.cs b/Refresh.Database/GameDatabaseContext.cs index a35eea0cd..338416d54 100644 --- a/Refresh.Database/GameDatabaseContext.cs +++ b/Refresh.Database/GameDatabaseContext.cs @@ -53,6 +53,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext internal DbSet GameAssets { get; set; } internal DbSet GameNotifications { get; set; } internal DbSet GamePhotos { get; set; } + internal DbSet GamePhotoSubjects { get; set; } internal DbSet GameIpVerificationRequests { get; set; } internal DbSet GameAnnouncements { get; set; } internal DbSet QueuedRegistrations { get; set; } diff --git a/Refresh.Database/Models/Photos/GamePhoto.cs b/Refresh.Database/Models/Photos/GamePhoto.cs index ff8741d11..2077989f2 100644 --- a/Refresh.Database/Models/Photos/GamePhoto.cs +++ b/Refresh.Database/Models/Photos/GamePhoto.cs @@ -43,92 +43,11 @@ public partial class GamePhoto : ISequentialId public string PlanHash { get; set; } #region Subjects - - [NotMapped] - public IReadOnlyList Subjects - { - get - { - List subjects = new(4); - - if (this.Subject1DisplayName != null) - subjects.Add(new GamePhotoSubject(this.Subject1User, this.Subject1DisplayName, this.Subject1Bounds)); - else return subjects; - - if (this.Subject2DisplayName != null) - subjects.Add(new GamePhotoSubject(this.Subject2User, this.Subject2DisplayName, this.Subject2Bounds)); - else return subjects; - - if (this.Subject3DisplayName != null) - subjects.Add(new GamePhotoSubject(this.Subject3User, this.Subject3DisplayName, this.Subject3Bounds)); - else return subjects; - - if (this.Subject4DisplayName != null) - subjects.Add(new GamePhotoSubject(this.Subject4User, this.Subject4DisplayName, this.Subject4Bounds)); - - return subjects; - } - set - { - if (value.Count > 4) throw new InvalidOperationException("Too many subjects. Should be caught beforehand by input validation"); - this.ClearSubjects(); - - if (value.Count >= 1) - { - this.Subject1User = value[0].User; - this.Subject1DisplayName = value[0].DisplayName; - foreach (float bound in value[0].Bounds) - this.Subject1Bounds.Add(bound); - } - - if (value.Count >= 2) - { - this.Subject2User = value[1].User; - this.Subject2DisplayName = value[1].DisplayName; - foreach (float bound in value[1].Bounds) - this.Subject2Bounds.Add(bound); - } - - if (value.Count >= 3) - { - this.Subject3User = value[2].User; - this.Subject3DisplayName = value[2].DisplayName; - foreach (float bound in value[2].Bounds) - this.Subject3Bounds.Add(bound); - } - - if (value.Count >= 4) - { - this.Subject4User = value[3].User; - this.Subject4DisplayName = value[3].DisplayName; - foreach (float bound in value[3].Bounds) - this.Subject4Bounds.Add(bound); - } - } - } - private void ClearSubjects() - { - this.Subject1User = null; - this.Subject1DisplayName = null; - this.Subject1Bounds.Clear(); - - this.Subject2User = null; - this.Subject2DisplayName = null; - this.Subject2Bounds.Clear(); - - this.Subject3User = null; - this.Subject3DisplayName = null; - this.Subject3Bounds.Clear(); - - this.Subject4User = null; - this.Subject4DisplayName = null; - this.Subject4Bounds.Clear(); - } - -#nullable enable - #pragma warning disable CS8618 // realm forces us to have a non-nullable IList so we have to have these shenanigans + #nullable restore + //private static string subjectObsoleteMessage = "All of these are obsolete due to GamePhotoSubjects now having their own DB table, " + // +"but we have to keep these attributes for migration anyway"; public ObjectId? Subject1UserId { get; set; } [ForeignKey(nameof(Subject1UserId))] public GameUser? Subject1User { get; set; } @@ -154,7 +73,6 @@ private void ClearSubjects() public List Subject3Bounds { get; set; } = []; public List Subject4Bounds { get; set; } = []; - #pragma warning restore CS8618 #nullable disable #endregion diff --git a/Refresh.Database/Models/Photos/GamePhotoSubject.cs b/Refresh.Database/Models/Photos/GamePhotoSubject.cs index 229a2792e..124203feb 100644 --- a/Refresh.Database/Models/Photos/GamePhotoSubject.cs +++ b/Refresh.Database/Models/Photos/GamePhotoSubject.cs @@ -1,25 +1,22 @@ -using System.Xml.Serialization; +using Refresh.Database.Helpers; using Refresh.Database.Models.Users; +using MongoDB.Bson; namespace Refresh.Database.Models.Photos; -[XmlRoot("subject")] -[XmlType("subject")] +[PrimaryKey(nameof(PhotoId), nameof(PlayerId))] public class GamePhotoSubject { -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. - [Obsolete("used for serialization. XML stuff should be moved to SerializedGamePhotoSubject", true)] - public GamePhotoSubject() {} + [ForeignKey(nameof(UserId))] + public GameUser? User { get; set; } + public ObjectId? UserId { get; set; } - public GamePhotoSubject(GameUser? user, string displayName, IList bounds) - { - this.User = user; - this.DisplayName = displayName; - this.Bounds = bounds; - } + [ForeignKey(nameof(PhotoId))] + [Required] public GamePhoto Photo { get; set; } = null!; + [Required] public int PhotoId { get; set; } - public GameUser? User { get; set; } - - public string DisplayName { get; set; } - public IList Bounds { get; } + [Required] public int PlayerId { get; set; } // player number + + public string DisplayName { get; set; } = ""; + public IList Bounds { get; set; } = new float[PhotoHelper.SubjectBoundaryCount]; } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs index 0357ee77f..807666de9 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs @@ -47,7 +47,7 @@ public class ApiGamePhotoResponse : IApiResponse, IDataConvertableFrom Date: Mon, 9 Mar 2026 18:03:55 +0100 Subject: [PATCH 02/11] Fix subject tests, add migrations --- .../20260309162631_SplitGamePhotoSubjects.cs | 55 +++++++++++++++++++ .../GameDatabaseContextModelSnapshot.cs | 43 +++++++++++++++ Refresh.Database/Models/Photos/GamePhoto.cs | 40 ++++++-------- .../MoveSubjectsOutOfGamePhotosMigration.cs | 22 ++++++++ .../RefreshWorkerManager.cs | 1 + .../Tests/Photos/PhotoEndpointsTests.cs | 20 ++++--- 6 files changed, 150 insertions(+), 31 deletions(-) create mode 100644 Refresh.Database/Migrations/20260309162631_SplitGamePhotoSubjects.cs create mode 100644 Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs diff --git a/Refresh.Database/Migrations/20260309162631_SplitGamePhotoSubjects.cs b/Refresh.Database/Migrations/20260309162631_SplitGamePhotoSubjects.cs new file mode 100644 index 000000000..687929b57 --- /dev/null +++ b/Refresh.Database/Migrations/20260309162631_SplitGamePhotoSubjects.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20260309162631_SplitGamePhotoSubjects")] + public partial class SplitGamePhotoSubjects : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GamePhotoSubjects", + columns: table => new + { + PhotoId = table.Column(type: "integer", nullable: false), + PlayerId = table.Column(type: "integer", nullable: false), + UserId = table.Column(type: "text", nullable: true), + DisplayName = table.Column(type: "text", nullable: false), + Bounds = table.Column(type: "real[]", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GamePhotoSubjects", x => new { x.PhotoId, x.PlayerId }); + table.ForeignKey( + name: "FK_GamePhotoSubjects_GamePhotos_PhotoId", + column: x => x.PhotoId, + principalTable: "GamePhotos", + principalColumn: "PhotoId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GamePhotoSubjects_GameUsers_UserId", + column: x => x.UserId, + principalTable: "GameUsers", + principalColumn: "UserId"); + }); + + migrationBuilder.CreateIndex( + name: "IX_GamePhotoSubjects_UserId", + table: "GamePhotoSubjects", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GamePhotoSubjects"); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index f57643c29..25abfcc5b 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -793,6 +793,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("GamePhotos"); }); + modelBuilder.Entity("Refresh.Database.Models.Photos.GamePhotoSubject", b => + { + b.Property("PhotoId") + .HasColumnType("integer"); + + b.Property("PlayerId") + .HasColumnType("integer"); + + b.PrimitiveCollection("Bounds") + .IsRequired() + .HasColumnType("real[]"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("PhotoId", "PlayerId"); + + b.HasIndex("UserId"); + + b.ToTable("GamePhotoSubjects"); + }); + modelBuilder.Entity("Refresh.Database.Models.Playlists.GamePlaylist", b => { b.Property("PlaylistId") @@ -2051,6 +2077,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Subject4User"); }); + modelBuilder.Entity("Refresh.Database.Models.Photos.GamePhotoSubject", b => + { + b.HasOne("Refresh.Database.Models.Photos.GamePhoto", "Photo") + .WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Refresh.Database.Models.Users.GameUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Photo"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Refresh.Database.Models.Playlists.GamePlaylist", b => { b.HasOne("Refresh.Database.Models.Users.GameUser", "Publisher") diff --git a/Refresh.Database/Models/Photos/GamePhoto.cs b/Refresh.Database/Models/Photos/GamePhoto.cs index 2077989f2..18051f76d 100644 --- a/Refresh.Database/Models/Photos/GamePhoto.cs +++ b/Refresh.Database/Models/Photos/GamePhoto.cs @@ -46,32 +46,28 @@ public partial class GamePhoto : ISequentialId #nullable restore - //private static string subjectObsoleteMessage = "All of these are obsolete due to GamePhotoSubjects now having their own DB table, " - // +"but we have to keep these attributes for migration anyway"; - public ObjectId? Subject1UserId { get; set; } - [ForeignKey(nameof(Subject1UserId))] - public GameUser? Subject1User { get; set; } - public string? Subject1DisplayName { get; set; } + private const string subjectObsoleteMessage = "GamePhotoSubjects are now in their own DB table. This attribute only exists to allow migration."; + + [Obsolete(subjectObsoleteMessage)] public ObjectId? Subject1UserId { get; set; } + [Obsolete(subjectObsoleteMessage)] public GameUser? Subject1User { get; set; } + [Obsolete(subjectObsoleteMessage)] public string? Subject1DisplayName { get; set; } - public ObjectId? Subject2UserId { get; set; } - [ForeignKey(nameof(Subject2UserId))] - public GameUser? Subject2User { get; set; } - public string? Subject2DisplayName { get; set; } + [Obsolete(subjectObsoleteMessage)] public ObjectId? Subject2UserId { get; set; } + [Obsolete(subjectObsoleteMessage)] public GameUser? Subject2User { get; set; } + [Obsolete(subjectObsoleteMessage)] public string? Subject2DisplayName { get; set; } - public ObjectId? Subject3UserId { get; set; } - [ForeignKey(nameof(Subject3UserId))] - public GameUser? Subject3User { get; set; } - public string? Subject3DisplayName { get; set; } + [Obsolete(subjectObsoleteMessage)] public ObjectId? Subject3UserId { get; set; } + [Obsolete(subjectObsoleteMessage)] public GameUser? Subject3User { get; set; } + [Obsolete(subjectObsoleteMessage)] public string? Subject3DisplayName { get; set; } - public ObjectId? Subject4UserId { get; set; } - [ForeignKey(nameof(Subject4UserId))] - public GameUser? Subject4User { get; set; } - public string? Subject4DisplayName { get; set; } + [Obsolete(subjectObsoleteMessage)] public ObjectId? Subject4UserId { get; set; } + [Obsolete(subjectObsoleteMessage)] public GameUser? Subject4User { get; set; } + [Obsolete(subjectObsoleteMessage)] public string? Subject4DisplayName { get; set; } - public List Subject1Bounds { get; set; } = []; - public List Subject2Bounds { get; set; } = []; - public List Subject3Bounds { get; set; } = []; - public List Subject4Bounds { get; set; } = []; + [Obsolete(subjectObsoleteMessage)] public List Subject1Bounds { get; set; } = []; + [Obsolete(subjectObsoleteMessage)] public List Subject2Bounds { get; set; } = []; + [Obsolete(subjectObsoleteMessage)] public List Subject3Bounds { get; set; } = []; + [Obsolete(subjectObsoleteMessage)] public List Subject4Bounds { get; set; } = []; #nullable disable diff --git a/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs b/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs new file mode 100644 index 000000000..d895199d3 --- /dev/null +++ b/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs @@ -0,0 +1,22 @@ +using Refresh.Database.Models.Photos; +using Refresh.Workers; + +namespace Refresh.Interfaces.Workers.Migrations; + +public class MoveSubjectsOutOfGamePhotosMigration : MigrationJob +{ + protected override IQueryable SortAndFilter(IQueryable query) + { + return query.OrderBy(p => p.PhotoId); + } + + protected override void Migrate(WorkContext context, GamePhoto[] batch) + { + foreach (GamePhoto photo in batch) + { + context.Database.MigratePhotoSubjects(photo, false); + } + + context.Database.SaveChanges(); + } +} \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs index 61088c8ae..b5307090e 100644 --- a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs +++ b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs @@ -27,6 +27,7 @@ public static WorkerManager Create(Logger logger, IDataStore dataStore, GameData manager.AddJob(); manager.AddJob(); manager.AddJob(); + manager.AddJob(); return manager; } diff --git a/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs b/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs index f6b406f0a..d392a3040 100644 --- a/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs +++ b/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs @@ -157,15 +157,17 @@ public void UploadPhotoAndValidateAttributes() Assert.That(gamePhoto!.LargeAssetHash, Is.EqualTo(TEST_ASSET_HASH)); Assert.That(gamePhoto!.PlanHash, Is.EqualTo(TEST_ASSET_HASH)); - Assert.That(gamePhoto!.Subjects.Count, Is.EqualTo(2)); - Assert.That(gamePhoto!.Subjects[0].Bounds, Is.EqualTo(PhotoHelper.ParseBoundsList("1,1,1,1"))); - Assert.That(gamePhoto!.Subjects[0].DisplayName, Is.EqualTo(user.Username)); - Assert.That(gamePhoto!.Subjects[0].User, Is.Not.Null); - Assert.That(gamePhoto!.Subjects[0].User!.UserId, Is.EqualTo(user.UserId)); - - Assert.That(gamePhoto!.Subjects[1].Bounds, Is.EqualTo(PhotoHelper.ParseBoundsList("2,4,5,6"))); - Assert.That(gamePhoto!.Subjects[1].DisplayName, Is.EqualTo("SecretAlt")); - Assert.That(gamePhoto!.Subjects[1].User, Is.Null); + List subjects = context.Database.GetSubjectsInPhoto(gamePhoto).ToList(); + + Assert.That(subjects.Count, Is.EqualTo(2)); + Assert.That(subjects[0].Bounds, Is.EqualTo(PhotoHelper.ParseBoundsList("1,1,1,1"))); + Assert.That(subjects[0].DisplayName, Is.EqualTo(user.Username)); + Assert.That(subjects[0].User, Is.Not.Null); + Assert.That(subjects[0].User!.UserId, Is.EqualTo(user.UserId)); + + Assert.That(subjects[1].Bounds, Is.EqualTo(PhotoHelper.ParseBoundsList("2,4,5,6"))); + Assert.That(subjects[1].DisplayName, Is.EqualTo("SecretAlt")); + Assert.That(subjects[1].User, Is.Null); } [Test] From 5208e0a4c1bfd577526137d8392093e4531e1a94 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 9 Mar 2026 18:13:18 +0100 Subject: [PATCH 03/11] Forgot to include this in the commits --- .../GameDatabaseContext.Photos.cs | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index f749e1306..1f95ed0b4 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -91,7 +91,7 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable + /// Migration only!! + /// + public void MigratePhotoSubjects(GamePhoto photo, bool saveChanges) + { + 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) + { + subjects.Add(new() + { + Photo = photo, + PlayerId = 4, + DisplayName = photo.Subject4DisplayName, + User = photo.Subject4User, + Bounds = photo.Subject4Bounds, + }); + } +#pragma warning restore CS0618 + + this.GamePhotoSubjects.AddRange(subjects); + if (saveChanges) this.SaveChanges(); + } + public void RemovePhoto(GamePhoto photo) { foreach (GameUser subjectUser in this.GetUsersInPhoto(photo)) From 5da24dd0a9b04dec89aef8ee537d53b9278409dc Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 9 Mar 2026 18:20:51 +0100 Subject: [PATCH 04/11] Re-include image GameAssets in GamePhotosIncluded --- Refresh.Database/GameDatabaseContext.Photos.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index 1f95ed0b4..d89aa2c16 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -12,6 +12,9 @@ namespace Refresh.Database; public partial class GameDatabaseContext // Photos { private IQueryable GamePhotosIncluded => this.GamePhotos + .Include(p => p.LargeAsset) + .Include(p => p.MediumAsset) + .Include(p => p.SmallAsset) .Include(p => p.Publisher) .Include(p => p.Publisher.Statistics) .Include(p => p.Level) From 301e90f7f3d1d38628640b072770a00412378ca2 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 9 Mar 2026 18:21:27 +0100 Subject: [PATCH 05/11] Fix compilation --- Refresh.Core/Extensions/PhotoExtensions.cs | 4 ++-- .../Types/Activity/SerializedEvents/SerializedEvent.cs | 2 +- .../Activity/SerializedEvents/SerializedPhotoUploadEvent.cs | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Refresh.Core/Extensions/PhotoExtensions.cs b/Refresh.Core/Extensions/PhotoExtensions.cs index 83e39d191..89d95fa19 100644 --- a/Refresh.Core/Extensions/PhotoExtensions.cs +++ b/Refresh.Core/Extensions/PhotoExtensions.cs @@ -19,10 +19,10 @@ public static SerializedPhoto FromGamePhoto(GamePhoto photo, DataContext dataCon MediumHash = dataContext.Database.GetAssetFromHash(photo.MediumAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.MediumAsset.AssetHash, SmallHash = dataContext.Database.GetAssetFromHash(photo.SmallAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.SmallAsset.AssetHash, PlanHash = photo.PlanHash, - PhotoSubjects = new List(photo.Subjects.Count), + PhotoSubjects = [], }; - foreach (GamePhotoSubject subject in photo.Subjects) + foreach (GamePhotoSubject subject in dataContext.Database.GetSubjectsInPhoto(photo).ToList()) { SerializedPhotoSubject newSubject = new() { diff --git a/Refresh.Interfaces.Game/Types/Activity/SerializedEvents/SerializedEvent.cs b/Refresh.Interfaces.Game/Types/Activity/SerializedEvents/SerializedEvent.cs index 4e07c9e8f..be047e361 100644 --- a/Refresh.Interfaces.Game/Types/Activity/SerializedEvents/SerializedEvent.cs +++ b/Refresh.Interfaces.Game/Types/Activity/SerializedEvents/SerializedEvent.cs @@ -77,7 +77,7 @@ public abstract class SerializedEvent : IDataConvertableFrom UsersInPhoto = []; - public static SerializedPhotoUploadEvent? FromSerializedLevelEvent(SerializedLevelEvent? e, GamePhoto? photo) + public static SerializedPhotoUploadEvent? FromSerializedLevelEvent(SerializedLevelEvent? e, GamePhoto? photo, DataContext dataContext) { if (e == null || photo == null) return null; @@ -21,7 +22,7 @@ public class SerializedPhotoUploadEvent : SerializedLevelEvent Type = e.Type, PhotoId = photo.PhotoId, - UsersInPhoto = photo.Subjects.Select(s => s.DisplayName).ToList(), + UsersInPhoto = dataContext.Database.GetSubjectsInPhoto(photo).Select(s => s.DisplayName).ToList(), }; } } \ No newline at end of file From 1943cf3eab07dc0c9554532d16fccbe74e0f8298 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 9 Mar 2026 18:32:01 +0100 Subject: [PATCH 06/11] Fix epic photo fails, improve subject bound parser exception messages --- Refresh.Database/GameDatabaseContext.Photos.cs | 9 ++++++--- Refresh.Database/Helpers/PhotoHelper.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index d89aa2c16..1d8871a50 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -31,7 +31,10 @@ public partial class GameDatabaseContext // Photos .Include(p => p.Photo!.Publisher.Statistics) .Include(p => p.Photo!.Level) .Include(p => p.Photo!.Level!.Publisher) - .Include(p => p.Photo!.Level!.Publisher!.Statistics); + .Include(p => p.Photo!.Level!.Publisher!.Statistics) + .Include(p => p.Photo!.LargeAsset) + .Include(p => p.Photo!.MediumAsset) + .Include(p => p.Photo!.SmallAsset); public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable subjects, GameUser publisher, GameLevel? level) { @@ -85,7 +88,7 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable { diff --git a/Refresh.Database/Helpers/PhotoHelper.cs b/Refresh.Database/Helpers/PhotoHelper.cs index 19db78956..f93918062 100644 --- a/Refresh.Database/Helpers/PhotoHelper.cs +++ b/Refresh.Database/Helpers/PhotoHelper.cs @@ -22,7 +22,7 @@ public static float[] ParseBoundsList(string input) string boundaryStr = boundsStr[i]; if (!float.TryParse(boundaryStr, NumberFormatInfo.InvariantInfo, out float f)) - throw new FormatException($"Boundary {boundaryStr} ({i+1}/{SubjectBoundaryCount}) is not a float"); + throw new FormatException($"Boundary '{boundaryStr}' ({i+1}/{SubjectBoundaryCount}) is not a float"); boundsParsed[i] = f; } From 0aa4865c0848d444ea5da6423ba31f5c90672216 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Tue, 10 Mar 2026 10:09:46 +0100 Subject: [PATCH 07/11] Just use one GamePhotoSubjectsIncluded --- Refresh.Database/GameDatabaseContext.Photos.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index 1d8871a50..90c3c9079 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -20,12 +20,10 @@ public partial class GameDatabaseContext // Photos .Include(p => p.Level) .Include(p => p.Level!.Publisher) .Include(p => p.Level!.Publisher!.Statistics); - - private IQueryable GamePhotoSubjectsIncludingUsers => this.GamePhotoSubjects + + private IQueryable GamePhotoSubjectsIncluded => this.GamePhotoSubjects .Include(p => p.User) - .Include(p => p.User!.Statistics); - - private IQueryable GamePhotoSubjectsIncludingPhotos => this.GamePhotoSubjects + .Include(p => p.User!.Statistics) .Include(p => p.Photo) .Include(p => p.Photo!.Publisher) .Include(p => p.Photo!.Publisher.Statistics) @@ -221,7 +219,7 @@ public void RemovePhoto(GamePhoto photo) } public IQueryable GetSubjectsInPhoto(GamePhoto photo) - => this.GamePhotoSubjectsIncludingUsers.Where(s => s.PhotoId == photo.PhotoId); + => this.GamePhotoSubjectsIncluded.Where(s => s.PhotoId == photo.PhotoId); public IQueryable GetUsersInPhoto(GamePhoto photo) => this.GetSubjectsInPhoto(photo) @@ -251,7 +249,7 @@ public int GetTotalPhotosByUser(GameUser user) [Pure] public DatabaseList GetPhotosWithUser(GameUser user, int count, int skip) => - new(this.GamePhotoSubjectsIncludingPhotos + new(this.GamePhotoSubjectsIncluded .Where(s => s.UserId == user.UserId) .Select(s => s.Photo), skip, count); From f5bdf00d3ac5061bc56e2f84995efb00d8138a42 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Tue, 10 Mar 2026 11:02:41 +0100 Subject: [PATCH 08/11] Turns out you can Include() lists of related entities --- Refresh.Core/Extensions/PhotoExtensions.cs | 2 +- Refresh.Database/GameDatabaseContext.Photos.cs | 6 ++++-- Refresh.Database/Models/Photos/GamePhoto.cs | 4 ++++ .../DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs | 2 +- .../Types/Activity/SerializedEvents/SerializedEvent.cs | 2 +- .../Activity/SerializedEvents/SerializedPhotoUploadEvent.cs | 5 ++--- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Refresh.Core/Extensions/PhotoExtensions.cs b/Refresh.Core/Extensions/PhotoExtensions.cs index 89d95fa19..a5aa6d72a 100644 --- a/Refresh.Core/Extensions/PhotoExtensions.cs +++ b/Refresh.Core/Extensions/PhotoExtensions.cs @@ -22,7 +22,7 @@ public static SerializedPhoto FromGamePhoto(GamePhoto photo, DataContext dataCon PhotoSubjects = [], }; - foreach (GamePhotoSubject subject in dataContext.Database.GetSubjectsInPhoto(photo).ToList()) + foreach (GamePhotoSubject subject in photo.Subjects) { SerializedPhotoSubject newSubject = new() { diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index 90c3c9079..06b265607 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -19,7 +19,8 @@ public partial class GameDatabaseContext // Photos .Include(p => p.Publisher.Statistics) .Include(p => p.Level) .Include(p => p.Level!.Publisher) - .Include(p => p.Level!.Publisher!.Statistics); + .Include(p => p.Level!.Publisher!.Statistics) + .Include(p => p.Subjects); private IQueryable GamePhotoSubjectsIncluded => this.GamePhotoSubjects .Include(p => p.User) @@ -32,7 +33,8 @@ public partial class GameDatabaseContext // Photos .Include(p => p.Photo!.Level!.Publisher!.Statistics) .Include(p => p.Photo!.LargeAsset) .Include(p => p.Photo!.MediumAsset) - .Include(p => p.Photo!.SmallAsset); + .Include(p => p.Photo!.SmallAsset) + .Include(p => p.Photo!.Subjects); public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable subjects, GameUser publisher, GameLevel? level) { diff --git a/Refresh.Database/Models/Photos/GamePhoto.cs b/Refresh.Database/Models/Photos/GamePhoto.cs index 18051f76d..d33115e20 100644 --- a/Refresh.Database/Models/Photos/GamePhoto.cs +++ b/Refresh.Database/Models/Photos/GamePhoto.cs @@ -43,6 +43,10 @@ public partial class GamePhoto : ISequentialId public string PlanHash { get; set; } #region Subjects + /// + /// A list of subjects, initialized using Include() + /// + public List Subjects { get; set; } #nullable restore diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs index 807666de9..0357ee77f 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/Photos/ApiGamePhotoResponse.cs @@ -47,7 +47,7 @@ public class ApiGamePhotoResponse : IApiResponse, IDataConvertableFrom UsersInPhoto = []; - public static SerializedPhotoUploadEvent? FromSerializedLevelEvent(SerializedLevelEvent? e, GamePhoto? photo, DataContext dataContext) + public static SerializedPhotoUploadEvent? FromSerializedLevelEvent(SerializedLevelEvent? e, GamePhoto? photo) { if (e == null || photo == null) return null; @@ -22,7 +21,7 @@ public class SerializedPhotoUploadEvent : SerializedLevelEvent Type = e.Type, PhotoId = photo.PhotoId, - UsersInPhoto = dataContext.Database.GetSubjectsInPhoto(photo).Select(s => s.DisplayName).ToList(), + UsersInPhoto = photo.Subjects.Select(s => s.DisplayName).ToList(), }; } } \ No newline at end of file From f923b607284ef4b35066fdea1bef1542896d58aa Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Tue, 10 Mar 2026 12:15:55 +0100 Subject: [PATCH 09/11] Sort subjects and photos with user --- Refresh.Database/GameDatabaseContext.Photos.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index 06b265607..b9ea6ec4a 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -20,7 +20,7 @@ public partial class GameDatabaseContext // Photos .Include(p => p.Level) .Include(p => p.Level!.Publisher) .Include(p => p.Level!.Publisher!.Statistics) - .Include(p => p.Subjects); + .Include(p => p.Subjects.OrderBy(s => s.PlayerId)); private IQueryable GamePhotoSubjectsIncluded => this.GamePhotoSubjects .Include(p => p.User) @@ -34,7 +34,7 @@ public partial class GameDatabaseContext // Photos .Include(p => p.Photo!.LargeAsset) .Include(p => p.Photo!.MediumAsset) .Include(p => p.Photo!.SmallAsset) - .Include(p => p.Photo!.Subjects); + .Include(p => p.Photo!.Subjects.OrderBy(s => s.PlayerId)); public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable subjects, GameUser publisher, GameLevel? level) { @@ -221,11 +221,14 @@ public void RemovePhoto(GamePhoto photo) } public IQueryable GetSubjectsInPhoto(GamePhoto photo) - => this.GamePhotoSubjectsIncluded.Where(s => s.PhotoId == photo.PhotoId); + => this.GamePhotoSubjectsIncluded + .Where(s => s.PhotoId == photo.PhotoId) + .OrderBy(s => s.PlayerId); public IQueryable GetUsersInPhoto(GamePhoto photo) => this.GetSubjectsInPhoto(photo) .Where(s => s.User != null) + .OrderBy(s => s.PlayerId) .Select(s => s.User!); public int GetTotalPhotoCount() => this.GamePhotos.Count(); @@ -253,7 +256,8 @@ public int GetTotalPhotosByUser(GameUser user) public DatabaseList GetPhotosWithUser(GameUser user, int count, int skip) => new(this.GamePhotoSubjectsIncluded .Where(s => s.UserId == user.UserId) - .Select(s => s.Photo), skip, count); + .Select(s => s.Photo) + .OrderByDescending(p => p.TakenAt), skip, count); [Pure] public int GetTotalPhotosWithUser(GameUser user) From a1416e9ba3501070a49b3a0f8586d33750fcafba Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Tue, 10 Mar 2026 13:27:29 +0100 Subject: [PATCH 10/11] Fix subject deletion when deleting user, add unit tests --- Refresh.Database/GameDatabaseContext.Users.cs | 3 +- RefreshTests.GameServer/TestContext.cs | 4 +- .../Tests/Photos/PhotoTests.cs | 124 ++++++++++++++++++ 3 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 RefreshTests.GameServer/Tests/Photos/PhotoTests.cs diff --git a/Refresh.Database/GameDatabaseContext.Users.cs b/Refresh.Database/GameDatabaseContext.Users.cs index 25b426724..1dadca80e 100644 --- a/Refresh.Database/GameDatabaseContext.Users.cs +++ b/Refresh.Database/GameDatabaseContext.Users.cs @@ -401,7 +401,7 @@ public void DeleteUser(GameUser user) user.PsnAuthenticationAllowed = false; user.RpcnAuthenticationAllowed = false; - foreach (GamePhotoSubject subject in this.GamePhotoSubjects.Where(s => s.UserId == user.UserId)) + foreach (GamePhotoSubject subject in this.GamePhotoSubjects.Where(s => s.UserId == user.UserId).ToList()) { subject.UserId = null; } @@ -410,7 +410,6 @@ public void DeleteUser(GameUser user) this.FavouriteUserRelations.RemoveRange(r => r.UserToFavourite == user); this.FavouriteUserRelations.RemoveRange(r => r.UserFavouriting == user); this.QueueLevelRelations.RemoveRange(r => r.User == user); - this.GamePhotoSubjects.RemoveRange(s => s.Photo.PublisherId == user.UserId); this.GamePhotos.RemoveRange(p => p.Publisher == user); this.GameUserVerifiedIpRelations.RemoveRange(p => p.User == user); diff --git a/RefreshTests.GameServer/TestContext.cs b/RefreshTests.GameServer/TestContext.cs index ac81046da..852b9dde5 100644 --- a/RefreshTests.GameServer/TestContext.cs +++ b/RefreshTests.GameServer/TestContext.cs @@ -145,7 +145,7 @@ public GameLevel CreateLevel(GameUser author, string title, string description, return level; } - public GamePhoto CreatePhotoWithSubject(GameUser author, string imageHash, GameLevel? level = null) + public GamePhoto CreatePhotoWithSubject(GameUser author, string imageHash, GameLevel? level = null, List? subjects = null) { // TODO: Return newly created GamePhoto if (this.Database.GetAssetFromHash(imageHash) == null) @@ -164,7 +164,7 @@ public GamePhoto CreatePhotoWithSubject(GameUser author, string imageHash, GameL LargeHash = imageHash, }; - IEnumerable subjects = + subjects ??= [ new() { diff --git a/RefreshTests.GameServer/Tests/Photos/PhotoTests.cs b/RefreshTests.GameServer/Tests/Photos/PhotoTests.cs new file mode 100644 index 000000000..7da190f89 --- /dev/null +++ b/RefreshTests.GameServer/Tests/Photos/PhotoTests.cs @@ -0,0 +1,124 @@ +using Refresh.Database.Helpers; +using Refresh.Database.Models.Levels; +using Refresh.Database.Models.Photos; +using Refresh.Database.Models.Users; + +namespace RefreshTests.GameServer.Tests.Photos; + +public class PhotoTests : GameServerTest +{ + private const string TEST_IMAGE_HASH = "0ec63b140374ba704a58fa0c743cb357683313dd"; + + [Test] + public void CreateAndGetPhotoWithSubjects() + { + using TestContext context = this.GetServer(); + GameUser publisher = context.CreateUser(); + GameUser player2 = context.CreateUser(); + GamePhoto createdPhoto = context.CreatePhotoWithSubject(publisher, TEST_IMAGE_HASH, subjects: + [ + new() + { + Username = publisher.Username, + DisplayName = publisher.Username, + BoundsList = "1,2,3,4", + }, + new() + { + Username = player2.Username, + DisplayName = player2.Username, + BoundsList = "4,5,6,7", + } + ]); + + List separateSubjects = context.Database.GetSubjectsInPhoto(createdPhoto).ToList(); + GamePhoto? fetchedPhoto = context.Database.GetRecentPhotos(10, 0).Items.FirstOrDefault(); + Assert.That(fetchedPhoto, Is.Not.Null); + Assert.That(fetchedPhoto!.PhotoId, Is.EqualTo(createdPhoto.PhotoId)); + + // these 3 subject lists are fetched/created in different ways, so ensure they all contain the correct data + Assert.That(separateSubjects.Count, Is.EqualTo(2)); + Assert.That(createdPhoto.Subjects.Count, Is.EqualTo(2)); + Assert.That(fetchedPhoto.Subjects.Count, Is.EqualTo(2)); + + // Assert first subject on all lists + Assert.That(separateSubjects[0].PlayerId, Is.EqualTo(1)); + Assert.That(createdPhoto.Subjects[0].PlayerId, Is.EqualTo(1)); + Assert.That(fetchedPhoto.Subjects[0].PlayerId, Is.EqualTo(1)); + + Assert.That(separateSubjects[0].DisplayName, Is.EqualTo(publisher.Username)); + Assert.That(createdPhoto.Subjects[0].DisplayName, Is.EqualTo(publisher.Username)); + Assert.That(fetchedPhoto.Subjects[0].DisplayName, Is.EqualTo(publisher.Username)); + + Assert.That(separateSubjects[0].User, Is.Not.Null); + Assert.That(createdPhoto.Subjects[0].User, Is.Not.Null); + Assert.That(fetchedPhoto.Subjects[0].User, Is.Not.Null); + + Assert.That(separateSubjects[0].User!.UserId, Is.EqualTo(publisher.UserId)); + Assert.That(createdPhoto.Subjects[0].User!.UserId, Is.EqualTo(publisher.UserId)); + Assert.That(fetchedPhoto.Subjects[0].User!.UserId, Is.EqualTo(publisher.UserId)); + + float[] subject1Bounds = PhotoHelper.ParseBoundsList("1,2,3,4"); + Assert.That(separateSubjects[0].Bounds, Is.EqualTo(subject1Bounds)); + Assert.That(createdPhoto.Subjects[0].Bounds, Is.EqualTo(subject1Bounds)); + Assert.That(fetchedPhoto.Subjects[0].Bounds, Is.EqualTo(subject1Bounds)); + + // Assert second subject on all lists + Assert.That(separateSubjects[1].PlayerId, Is.EqualTo(2)); + Assert.That(createdPhoto.Subjects[1].PlayerId, Is.EqualTo(2)); + Assert.That(fetchedPhoto.Subjects[1].PlayerId, Is.EqualTo(2)); + + Assert.That(separateSubjects[1].DisplayName, Is.EqualTo(player2.Username)); + Assert.That(createdPhoto.Subjects[1].DisplayName, Is.EqualTo(player2.Username)); + Assert.That(fetchedPhoto.Subjects[1].DisplayName, Is.EqualTo(player2.Username)); + + Assert.That(separateSubjects[1].User, Is.Not.Null); + Assert.That(createdPhoto.Subjects[1].User, Is.Not.Null); + Assert.That(fetchedPhoto.Subjects[1].User, Is.Not.Null); + + Assert.That(separateSubjects[1].User!.UserId, Is.EqualTo(player2.UserId)); + Assert.That(createdPhoto.Subjects[1].User!.UserId, Is.EqualTo(player2.UserId)); + Assert.That(fetchedPhoto.Subjects[1].User!.UserId, Is.EqualTo(player2.UserId)); + + float[] subject2Bounds = PhotoHelper.ParseBoundsList("4,5,6,7"); + Assert.That(separateSubjects[1].Bounds, Is.EqualTo(subject2Bounds)); + Assert.That(createdPhoto.Subjects[1].Bounds, Is.EqualTo(subject2Bounds)); + Assert.That(fetchedPhoto.Subjects[1].Bounds, Is.EqualTo(subject2Bounds)); + } + + [Test] + public void DeletePhotoAndSubjectsWhenDeletingUser() + { + using TestContext context = this.GetServer(); + GameUser publisher = context.CreateUser(); + GameUser subject = context.CreateUser(); + GamePhoto photo = context.CreatePhotoWithSubject(publisher, TEST_IMAGE_HASH, subjects: + [ + new() + { + Username = publisher.Username, + DisplayName = publisher.Username, + BoundsList = "1,2,3,4", + }, + new() + { + Username = subject.Username, + DisplayName = subject.Username, + BoundsList = "4,5,6,7", + } + ]); + + // Initial subjects checks + List subjects = context.Database.GetSubjectsInPhoto(photo).ToList(); + Assert.That(subjects.Count, Is.EqualTo(2)); + Assert.That(photo.Subjects.Count, Is.EqualTo(2)); + + // Delete publisher and re-check + context.Database.DeleteUser(publisher); + context.Database.Refresh(); + + // GetSubjectsInPhoto only compares the photo IDs, so if the subjects weren't cascade-deleted, they should still be found + subjects = context.Database.GetSubjectsInPhoto(photo).ToList(); + Assert.That(subjects.Count, Is.Zero); + } +} \ No newline at end of file From 6c619622a02c48478795d6fa7a9f889084996e37 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Tue, 10 Mar 2026 13:28:23 +0100 Subject: [PATCH 11/11] Include subject list in UploadPhoto's return value --- Refresh.Database/GameDatabaseContext.Photos.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index b9ea6ec4a..1e547b1a5 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -114,6 +114,8 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable