diff --git a/src/Data/UserDataStore.cs b/src/Data/UserDataStore.cs index b21eb73..661d398 100644 --- a/src/Data/UserDataStore.cs +++ b/src/Data/UserDataStore.cs @@ -92,6 +92,24 @@ public class UserDataStore(DarkMasterMemoryDatabase masterDb) public bool TryGet(long userId, out DarkUserMemoryDatabase db) => _users.TryGetValue(userId, out db!); + /// + /// Finds the user database whose EntityIUser record matches the given playerId. + /// Returns false if no user has that playerId. + /// + public bool TryGetByPlayerId(long playerId, out DarkUserMemoryDatabase db) + { + foreach (var (_, userDb) in _users) + { + if (userDb.EntityIUser.Any(u => u.PlayerId == playerId)) + { + db = userDb; + return true; + } + } + db = null!; + return false; + } + /// /// Stores a user database, replacing any existing one for that userId. /// diff --git a/src/Services/UserService.cs b/src/Services/UserService.cs index d45afb0..c0b8bc5 100644 --- a/src/Services/UserService.cs +++ b/src/Services/UserService.cs @@ -11,6 +11,9 @@ namespace MariesWonderland.Services; public class UserService(UserDataStore store, UserDataSeeder seeder) : MariesWonderland.Proto.User.UserService.UserServiceBase { + public const string AndroidApiKey = "1234567890"; + public const string AndroidNonce = "Mama"; + private readonly UserDataStore _store = store; private readonly UserDataSeeder _seeder = seeder; @@ -19,8 +22,8 @@ public class UserService(UserDataStore store, UserDataSeeder seeder) : MariesWon { return Task.FromResult(new GetAndroidArgsResponse { - ApiKey = "1234567890", - Nonce = "Mama" + ApiKey = AndroidApiKey, + Nonce = AndroidNonce }); } @@ -166,41 +169,33 @@ public class UserService(UserDataStore store, UserDataSeeder seeder) : MariesWon /// Returns a player's profile including name, level, favorite costume, and lead deck character. public override Task GetUserProfile(GetUserProfileRequest request, ServerCallContext context) { - long userId = request.PlayerId != 0 ? request.PlayerId : context.GetUserId(); + long callerUserId = context.GetUserId(); - if (!_store.TryGet(userId, out DarkUserMemoryDatabase userDb)) - { - return Task.FromResult(new GetUserProfileResponse - { - LatestUsedDeck = new ProfileDeck { Power = 100 }, - PvpInfo = new ProfilePvpInfo(), - GamePlayHistory = new GamePlayHistory - { - HistoryItem = { }, - HistoryCategoryGraphItem = { } - } - }); - } + if (!_store.TryGetByPlayerId(request.PlayerId, out DarkUserMemoryDatabase targetDb)) + return Task.FromResult(new GetUserProfileResponse()); - EntityIUserProfile? profile = userDb.EntityIUserProfile.FirstOrDefault(p => p.UserId == userId); - EntityIUserStatus? status = userDb.EntityIUserStatus.FirstOrDefault(s => s.UserId == userId); + EntityIUserProfile? profile = targetDb.EntityIUserProfile.FirstOrDefault(p => p.UserId == targetDb.UserId); + EntityIUserStatus? status = targetDb.EntityIUserStatus.FirstOrDefault(s => s.UserId == targetDb.UserId); + bool isOwnProfile = targetDb.UserId == callerUserId; + int maxDeckPower = 0; List deckCharacters = []; - EntityIUserDeck? deck = userDb.EntityIUserDeck.FirstOrDefault(d => + EntityIUserDeck? deck = targetDb.EntityIUserDeck.FirstOrDefault(d => d.DeckType == DeckType.QUEST && d.UserDeckNumber == 1); if (deck != null && !string.IsNullOrEmpty(deck.UserDeckCharacterUuid01)) { - EntityIUserDeckCharacter? dc = userDb.EntityIUserDeckCharacter + EntityIUserDeckCharacter? dc = targetDb.EntityIUserDeckCharacter .FirstOrDefault(c => c.UserDeckCharacterUuid == deck.UserDeckCharacterUuid01); if (dc != null) { - int costumeId = userDb.EntityIUserCostume + maxDeckPower = dc.Power; + int costumeId = targetDb.EntityIUserCostume .FirstOrDefault(c => c.UserCostumeUuid == dc.UserCostumeUuid)?.CostumeId ?? 0; - EntityIUserWeapon? weapon = userDb.EntityIUserWeapon + EntityIUserWeapon? weapon = targetDb.EntityIUserWeapon .FirstOrDefault(w => w.UserWeaponUuid == dc.MainUserWeaponUuid); deckCharacters.Add(new ProfileDeckCharacter @@ -215,16 +210,22 @@ public class UserService(UserDataStore store, UserDataSeeder seeder) : MariesWon return Task.FromResult(new GetUserProfileResponse { Level = status?.Level ?? 0, - Name = profile?.Name ?? "", + Name = profile?.Name ?? string.Empty, FavoriteCostumeId = profile?.FavoriteCostumeId ?? 0, - Message = profile?.Message ?? "", + Message = profile?.Message ?? string.Empty, IsFriend = false, LatestUsedDeck = new ProfileDeck { - Power = 100, + Power = maxDeckPower, DeckCharacter = { deckCharacters } }, - PvpInfo = new ProfilePvpInfo(), + PvpInfo = new ProfilePvpInfo() + { + MaxSeasonRank = targetDb.EntityIUserPvpWeeklyResult + .GroupBy(x => x.PvpSeasonId) + .DefaultIfEmpty() + .Min(x => x?.OrderBy(y => y.PvpWeeklyVersion).Select(y => y.FinalRank).LastOrDefault() ?? 0), + }, GamePlayHistory = new GamePlayHistory { HistoryItem = { }, diff --git a/tests/Services/UserServiceTests.cs b/tests/Services/UserServiceTests.cs new file mode 100644 index 0000000..1b5f468 --- /dev/null +++ b/tests/Services/UserServiceTests.cs @@ -0,0 +1,411 @@ +using Google.Protobuf.WellKnownTypes; +using MariesWonderland.Configuration; +using MariesWonderland.Data; +using MariesWonderland.Models.Entities; +using MariesWonderland.Models.Type; +using MariesWonderland.Proto.User; +using MariesWonderland.Tests.Infrastructure; +using Microsoft.Extensions.Options; +using UserService = MariesWonderland.Services.UserService; + +namespace MariesWonderland.Tests.Services; + +public class UserServiceTests(MasterDatabaseFixture fixture) : ServiceTestBase(fixture), IClassFixture +{ + private const long UserId = 1L; + + private static UserService CreateService(UserDataStore store) + { + var options = Options.Create(new ServerOptions()); + var seeder = new UserDataSeeder(options); + return new UserService(store, seeder); + } + + /// + /// Verifies that SetUserName updates the profile name and sets the name update timestamp. + /// + [Fact] + public async Task SetUserName_WithExistingProfile_UpdatesNameAndTimestamp() + { + var userDb = CreateUserDb(); + userDb.EntityIUserProfile.Add(new EntityIUserProfile + { + UserId = UserId, + Name = "OldName" + }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.SetUserName( + new SetUserNameRequest { Name = "NewName" }, + ContextFor(UserId)); + + Assert.NotNull(response); + Assert.Equal("NewName", userDb.EntityIUserProfile[0].Name); + Assert.True(userDb.EntityIUserProfile[0].NameUpdateDatetime > 0); + } + + /// + /// Verifies that SetUserSetting updates the notification preference on the setting record. + /// + [Fact] + public async Task SetUserSetting_WithExistingSetting_UpdatesNotifyPurchaseAlert() + { + var userDb = CreateUserDb(); + userDb.EntityIUserSetting.Add(new EntityIUserSetting + { + UserId = UserId, + IsNotifyPurchaseAlert = false + }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.SetUserSetting( + new SetUserSettingRequest { IsNotifyPurchaseAlert = true }, + ContextFor(UserId)); + + Assert.NotNull(response); + Assert.True(userDb.EntityIUserSetting[0].IsNotifyPurchaseAlert); + } + + /// + /// Verifies that SetUserMessage updates the profile message and sets the message update timestamp. + /// + [Fact] + public async Task SetUserMessage_WithExistingProfile_UpdatesMessageAndTimestamp() + { + var userDb = CreateUserDb(); + userDb.EntityIUserProfile.Add(new EntityIUserProfile + { + UserId = UserId, + Name = "TestUser" + }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.SetUserMessage( + new SetUserMessageRequest { Message = "Hello World" }, + ContextFor(UserId)); + + Assert.NotNull(response); + Assert.Equal("Hello World", userDb.EntityIUserProfile[0].Message); + Assert.True(userDb.EntityIUserProfile[0].MessageUpdateDatetime > 0); + } + + /// + /// Verifies that SetUserFavoriteCostumeId updates the profile's favorite costume and timestamp. + /// + [Fact] + public async Task SetUserFavoriteCostumeId_WithExistingProfile_UpdatesCostumeIdAndTimestamp() + { + var userDb = CreateUserDb(); + userDb.EntityIUserProfile.Add(new EntityIUserProfile + { + UserId = UserId, + Name = "TestUser" + }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.SetUserFavoriteCostumeId( + new SetUserFavoriteCostumeIdRequest { FavoriteCostumeId = 42 }, + ContextFor(UserId)); + + Assert.NotNull(response); + Assert.Equal(42, userDb.EntityIUserProfile[0].FavoriteCostumeId); + Assert.True(userDb.EntityIUserProfile[0].FavoriteCostumeIdUpdateDatetime > 0); + } + + /// + /// Verifies that SetBirthYearMonth updates the user's birth year and month. + /// + [Fact] + public async Task SetBirthYearMonth_WithExistingUser_UpdatesBirthYearAndMonth() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.SetBirthYearMonth( + new SetBirthYearMonthRequest { BirthYear = 1990, BirthMonth = 6 }, + ContextFor(UserId)); + + Assert.NotNull(response); + Assert.Equal(1990, userDb.EntityIUser[0].BirthYear); + Assert.Equal(6, userDb.EntityIUser[0].BirthMonth); + } + + /// + /// Verifies that GetBirthYearMonth returns the stored birth year and month. + /// + [Fact] + public async Task GetBirthYearMonth_WithExistingUser_ReturnsStoredValues() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser + { + UserId = UserId, + BirthYear = 1985, + BirthMonth = 3 + }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetBirthYearMonth( + new Empty(), + ContextFor(UserId)); + + Assert.Equal(1985, response.BirthYear); + Assert.Equal(3, response.BirthMonth); + } + + /// + /// Verifies that GetChargeMoney returns the stored charge money amount. + /// + [Fact] + public async Task GetChargeMoney_WithExistingSUser_ReturnsChargeMoneyThisMonth() + { + var userDb = CreateUserDb(); + userDb.EntitySUser.Add(new EntitySUser + { + UserId = UserId, + ChargeMoneyThisMonth = 500 + }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetChargeMoney( + new Empty(), + ContextFor(UserId)); + + Assert.Equal(500, response.ChargeMoneyThisMonth); + } + + /// + /// Verifies that GetChargeMoney returns zero when no EntitySUser record exists. + /// + [Fact] + public async Task GetChargeMoney_WithNoSUser_ReturnsZero() + { + var userDb = CreateUserDb(); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetChargeMoney( + new Empty(), + ContextFor(UserId)); + + Assert.Equal(0, response.ChargeMoneyThisMonth); + } + + [Fact] + public async Task GetUserProfile_ReturnsLevelFromStatus() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId, PlayerId = 12345L }); + userDb.EntityIUserStatus.Add(new EntityIUserStatus { UserId = UserId, Level = 42 }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetUserProfile( + new GetUserProfileRequest { PlayerId = 12345L }, + ContextFor(UserId)); + + Assert.Equal(42, response.Level); + } + + [Fact] + public async Task GetUserProfile_ReturnsNameFromProfile() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId, PlayerId = 12345L }); + userDb.EntityIUserProfile.Add(new EntityIUserProfile { UserId = UserId, Name = "TestPlayer" }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetUserProfile( + new GetUserProfileRequest { PlayerId = 12345L }, + ContextFor(UserId)); + + Assert.Equal("TestPlayer", response.Name); + } + + [Fact] + public async Task GetUserProfile_ReturnsFavoriteCostumeIdFromProfile() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId, PlayerId = 12345L }); + userDb.EntityIUserProfile.Add(new EntityIUserProfile { UserId = UserId, FavoriteCostumeId = 99 }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetUserProfile( + new GetUserProfileRequest { PlayerId = 12345L }, + ContextFor(UserId)); + + Assert.Equal(99, response.FavoriteCostumeId); + } + + [Fact] + public async Task GetUserProfile_ReturnsMessageFromProfile() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId, PlayerId = 12345L }); + userDb.EntityIUserProfile.Add(new EntityIUserProfile { UserId = UserId, Message = "Hello!" }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetUserProfile( + new GetUserProfileRequest { PlayerId = 12345L }, + ContextFor(UserId)); + + Assert.Equal("Hello!", response.Message); + } + + [Fact] + public async Task GetUserProfile_ReturnsPopulatedLatestUsedDeck() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId, PlayerId = 12345L }); + userDb.EntityIUserDeck.Add(new EntityIUserDeck + { + UserId = UserId, + DeckType = DeckType.QUEST, + UserDeckNumber = 1, + UserDeckCharacterUuid01 = "dc-uuid" + }); + userDb.EntityIUserDeckCharacter.Add(new EntityIUserDeckCharacter + { + UserId = UserId, + UserDeckCharacterUuid = "dc-uuid", + Power = 5000, + UserCostumeUuid = "c-uuid", + MainUserWeaponUuid = "w-uuid" + }); + userDb.EntityIUserCostume.Add(new EntityIUserCostume + { + UserId = UserId, + UserCostumeUuid = "c-uuid", + CostumeId = 200100 + }); + userDb.EntityIUserWeapon.Add(new EntityIUserWeapon + { + UserId = UserId, + UserWeaponUuid = "w-uuid", + WeaponId = 300100, + Level = 15 + }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetUserProfile( + new GetUserProfileRequest { PlayerId = 12345L }, + ContextFor(UserId)); + + Assert.Equal(5000, response.LatestUsedDeck.Power); + Assert.Single(response.LatestUsedDeck.DeckCharacter); + Assert.Equal(200100, response.LatestUsedDeck.DeckCharacter[0].CostumeId); + Assert.Equal(300100, response.LatestUsedDeck.DeckCharacter[0].MainWeaponId); + Assert.Equal(15, response.LatestUsedDeck.DeckCharacter[0].MainWeaponLevel); + } + + [Fact] + public async Task GetUserProfile_ReturnsMaxSeasonRankFromPvpResults() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId, PlayerId = 12345L }); + userDb.EntityIUserPvpWeeklyResult.Add(new EntityIUserPvpWeeklyResult + { + UserId = UserId, + PvpSeasonId = 1, + PvpWeeklyVersion = 1, + FinalRank = 50 + }); + userDb.EntityIUserPvpWeeklyResult.Add(new EntityIUserPvpWeeklyResult + { + UserId = UserId, + PvpSeasonId = 1, + PvpWeeklyVersion = 2, + FinalRank = 30 + }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetUserProfile( + new GetUserProfileRequest { PlayerId = 12345L }, + ContextFor(UserId)); + + Assert.Equal(30, response.PvpInfo.MaxSeasonRank); + } + + [Fact] + public async Task GetUserProfile_UnknownPlayerId_ReturnsEmptyResponse() + { + var userDb = CreateUserDb(); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + var response = await service.GetUserProfile( + new GetUserProfileRequest { PlayerId = 99999L }, + ContextFor(UserId)); + + Assert.Equal(string.Empty, response.Name); + Assert.Equal(0, response.Level); + } + + [Fact] + public async Task GetAndroidArgs_ReturnsExpectedApiKeyAndNonce() + { + var store = CreateStore(UserId, CreateUserDb(), MasterDb); + var service = CreateService(store); + + var response = await service.GetAndroidArgs(new GetAndroidArgsRequest(), ContextFor(UserId)); + + Assert.Equal(UserService.AndroidApiKey, response.ApiKey); + Assert.Equal(UserService.AndroidNonce, response.Nonce); + } + + [Fact] + public async Task GameStart_SetsGameStartDatetimeOnEntityIUser() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + await service.GameStart(new Empty(), ContextFor(UserId)); + + Assert.True(userDb.EntityIUser[0].GameStartDatetime > 0); + } + + [Fact] + public async Task GameStart_CreatesEntityIUserGemIfNoneExists() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + await service.GameStart(new Empty(), ContextFor(UserId)); + + Assert.Single(userDb.EntityIUserGem); + Assert.Equal(0, userDb.EntityIUserGem[0].PaidGem); + Assert.Equal(0, userDb.EntityIUserGem[0].FreeGem); + } + + [Fact] + public async Task GameStart_DoesNotCreateDuplicateEntityIUserGem() + { + var userDb = CreateUserDb(); + userDb.EntityIUser.Add(new EntityIUser { UserId = UserId }); + userDb.EntityIUserGem.Add(new EntityIUserGem { UserId = UserId, PaidGem = 100, FreeGem = 50 }); + var store = CreateStore(UserId, userDb, MasterDb); + var service = CreateService(store); + + await service.GameStart(new Empty(), ContextFor(UserId)); + + Assert.Single(userDb.EntityIUserGem); + Assert.Equal(100, userDb.EntityIUserGem[0].PaidGem); + } +}