mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
122 lines
5.1 KiB
C#
122 lines
5.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Storage;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.RecNet.Config;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// The startup GameConfig passes share one rule: a row an earlier pass added to the SAME
|
|
/// DbContext is not in the database yet, so a plain query for its key answers null - and the
|
|
/// "make sure this key exists" step that follows then adds a SECOND instance of it. EF
|
|
/// refuses that the moment the duplicate is tracked, and the refusal is a crash, not a
|
|
/// warning. It is how the first start against an empty database died:
|
|
///
|
|
/// Unhandled exception. System.InvalidOperationException: The instance of entity type
|
|
/// 'ConfigVal' cannot be tracked because another instance with the same key value for
|
|
/// {'Key'} is already being tracked.
|
|
/// at Program.<<Main>$>g__EnsureSplitTestHardOverrides|0_32(RecDBContext dbContext)
|
|
///
|
|
/// `EnsureRequiredGameConfigs` upserts `splitTestHardOverrides` and the ensure pass two lines
|
|
/// later re-added it. GameConfigSeeder is the tracked-aware lookup that closes the gap; these
|
|
/// cases hold it in place, and the same trap was armed behind it for
|
|
/// `splitTestsAvailable`/`splitTestSegmentProbabilities` (RepairSplitTestConfigs) and the
|
|
/// `UGC.Room2*` batch (EnableBetaCreativeToolsConfigs).
|
|
/// </summary>
|
|
public class GameConfigSeederTests
|
|
{
|
|
private const string HardOverridesKey = "splitTestHardOverrides";
|
|
|
|
/// <summary>Shared so two contexts can mean the same store, the way two passes of one start do.</summary>
|
|
private static readonly InMemoryDatabaseRoot Root = new();
|
|
|
|
private static RecDBContext CreateDb(string name) => new(
|
|
TestDbOptions.Create()
|
|
.UseInMemoryDatabase(name, Root)
|
|
.Options);
|
|
|
|
private static RecDBContext CreateDb() => CreateDb(Guid.NewGuid().ToString());
|
|
|
|
[Fact]
|
|
public async Task AddingTheSameKeyAfterAQuery_IsTheConflictTheSeederExistsToPrevent()
|
|
{
|
|
using var db = CreateDb();
|
|
await db.GameConfig.AddAsync(new ConfigVal(HardOverridesKey, "A=1"));
|
|
|
|
// The trap: a row this context added but has not saved is invisible to a query.
|
|
Assert.Null(await db.GameConfig.FirstOrDefaultAsync(x => x.Key == HardOverridesKey));
|
|
|
|
// And the duplicate a plain lookup would then add is fatal, not cosmetic.
|
|
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
|
await db.GameConfig.AddAsync(new ConfigVal(HardOverridesKey, "A=1;B=2")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FindAsync_SeesARowThisContextAdded_WithoutSavingIt()
|
|
{
|
|
using var db = CreateDb();
|
|
await db.GameConfig.AddAsync(new ConfigVal(HardOverridesKey, "A=1"));
|
|
|
|
var found = await GameConfigSeeder.FindAsync(db, HardOverridesKey);
|
|
|
|
Assert.NotNull(found);
|
|
Assert.Equal("A=1", found!.Value);
|
|
Assert.Same(db.GameConfig.Local.Single(), found);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpsertAsync_TwiceInOnePass_TracksOneRowAndKeepsBothWrites()
|
|
{
|
|
// The literal shape of the crash: one pass upserts the key, the next looks it up and
|
|
// (before the seeder) would have added a second ConfigVal carrying the same key.
|
|
using var db = CreateDb();
|
|
await GameConfigSeeder.UpsertAsync(db, HardOverridesKey, "A=1");
|
|
var existing = await GameConfigSeeder.FindAsync(db, HardOverridesKey);
|
|
|
|
Assert.NotNull(existing);
|
|
existing!.Value = "A=1;B=2";
|
|
await db.SaveChangesAsync();
|
|
|
|
Assert.Single(db.ChangeTracker.Entries<ConfigVal>());
|
|
Assert.Equal("A=1;B=2", (await db.GameConfig.SingleAsync()).Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FindAsync_ReadsARowAnotherContextStored()
|
|
{
|
|
var name = Guid.NewGuid().ToString();
|
|
using (var seeding = CreateDb(name))
|
|
{
|
|
await GameConfigSeeder.UpsertAsync(seeding, HardOverridesKey, "A=1");
|
|
await seeding.SaveChangesAsync();
|
|
}
|
|
|
|
using var db = CreateDb(name);
|
|
Assert.Null(db.GameConfig.Local.FirstOrDefault()); // nothing tracked yet
|
|
|
|
var found = await GameConfigSeeder.FindAsync(db, HardOverridesKey);
|
|
|
|
Assert.NotNull(found);
|
|
Assert.Equal("A=1", found!.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpsertAsync_FillsABlankStoredValue_ButLeavesAnEditedOneAlone()
|
|
{
|
|
using var db = CreateDb();
|
|
|
|
await GameConfigSeeder.UpsertAsync(db, "splitTestSoftOverrides", "A=1");
|
|
await GameConfigSeeder.UpsertAsync(db, "splitTestSoftOverrides", "B=2");
|
|
Assert.Equal("A=1", (await GameConfigSeeder.FindAsync(db, "splitTestSoftOverrides"))!.Value);
|
|
|
|
await GameConfigSeeder.UpsertAsync(db, "UGC.MaxChipsVisible", "");
|
|
await GameConfigSeeder.UpsertAsync(db, "UGC.MaxChipsVisible", "5000");
|
|
Assert.Equal("5000", (await GameConfigSeeder.FindAsync(db, "UGC.MaxChipsVisible"))!.Value);
|
|
|
|
await GameConfigSeeder.UpsertAsync(db, "UGC.MaxChipsVisible", "9000");
|
|
Assert.Equal("5000", (await GameConfigSeeder.FindAsync(db, "UGC.MaxChipsVisible"))!.Value);
|
|
}
|
|
}
|