mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 23:49:02 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user