Fix up UserService APIs and add initial unit tests

This commit is contained in:
BillyCool
2026-04-21 02:55:03 +10:00
parent 352f97682c
commit 235688520f
3 changed files with 456 additions and 26 deletions

View File

@@ -92,6 +92,24 @@ public class UserDataStore(DarkMasterMemoryDatabase masterDb)
public bool TryGet(long userId, out DarkUserMemoryDatabase db)
=> _users.TryGetValue(userId, out db!);
/// <summary>
/// Finds the user database whose EntityIUser record matches the given playerId.
/// Returns false if no user has that playerId.
/// </summary>
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;
}
/// <summary>
/// Stores a user database, replacing any existing one for that userId.
/// </summary>

View File

@@ -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
/// <summary>Returns a player's profile including name, level, favorite costume, and lead deck character.</summary>
public override Task<GetUserProfileResponse> 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<ProfileDeckCharacter> 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 = { },

View File

@@ -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<MasterDatabaseFixture>
{
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);
}
/// <summary>
/// Verifies that SetUserName updates the profile name and sets the name update timestamp.
/// </summary>
[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);
}
/// <summary>
/// Verifies that SetUserSetting updates the notification preference on the setting record.
/// </summary>
[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);
}
/// <summary>
/// Verifies that SetUserMessage updates the profile message and sets the message update timestamp.
/// </summary>
[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);
}
/// <summary>
/// Verifies that SetUserFavoriteCostumeId updates the profile's favorite costume and timestamp.
/// </summary>
[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);
}
/// <summary>
/// Verifies that SetBirthYearMonth updates the user's birth year and month.
/// </summary>
[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);
}
/// <summary>
/// Verifies that GetBirthYearMonth returns the stored birth year and month.
/// </summary>
[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);
}
/// <summary>
/// Verifies that GetChargeMoney returns the stored charge money amount.
/// </summary>
[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);
}
/// <summary>
/// Verifies that GetChargeMoney returns zero when no EntitySUser record exists.
/// </summary>
[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);
}
}