final
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
using BlueLaminate.Core.Tradeups;
|
||||
using BlueLaminate.EFCore.Data;
|
||||
using BlueLaminate.EFCore.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace BlueLaminate.Tests.Tradeups;
|
||||
|
||||
/// <summary>
|
||||
/// Graph-derivation rules verified against a synthetic in-memory catalogue — never the
|
||||
/// live database (see the no-live-DB-perturbation rule). Each test seeds the minimal
|
||||
/// catalogue shape it needs.
|
||||
/// </summary>
|
||||
public class TradeupGraphBuilderTests
|
||||
{
|
||||
private static SkinTrackerDbContext NewContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<SkinTrackerDbContext>()
|
||||
.UseInMemoryDatabase($"tradeups-{Guid.NewGuid()}")
|
||||
.Options;
|
||||
return new SkinTrackerDbContext(options);
|
||||
}
|
||||
|
||||
private static async Task<TradeupGraph> BuildAsync(SkinTrackerDbContext db)
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
var builder = new TradeupGraphBuilder(db, NullLogger<TradeupGraphBuilder>.Instance);
|
||||
return await builder.BuildAsync();
|
||||
}
|
||||
|
||||
private sealed class Catalogue
|
||||
{
|
||||
private int _nextId = 1;
|
||||
public Weapon Rifle { get; } = new() { Id = 1000, Name = "AK-47", Type = "Rifle", Team = "Both" };
|
||||
public Weapon Knife { get; } = new() { Id = 1001, Name = "Karambit", Type = "Knives", Team = "Both" };
|
||||
public Weapon Glove { get; } = new() { Id = 1002, Name = "Sport Gloves", Type = "Gloves", Team = "Both" };
|
||||
|
||||
public List<Skin> Skins { get; } = new();
|
||||
|
||||
public Skin Add(
|
||||
Collection collection,
|
||||
string rarity,
|
||||
Weapon? weapon = null,
|
||||
decimal? floatMin = 0.0m,
|
||||
decimal? floatMax = 1.0m)
|
||||
{
|
||||
var skin = new Skin
|
||||
{
|
||||
Id = _nextId,
|
||||
Slug = $"skin-{_nextId}",
|
||||
Name = $"Skin {_nextId}",
|
||||
Rarity = rarity,
|
||||
Weapon = weapon ?? Rifle,
|
||||
FloatMin = floatMin,
|
||||
FloatMax = floatMax,
|
||||
Collections = new List<Collection> { collection },
|
||||
};
|
||||
_nextId++;
|
||||
Skins.Add(skin);
|
||||
return skin;
|
||||
}
|
||||
}
|
||||
|
||||
private static Collection NewCollection(string name, string type = "Collection")
|
||||
=> new() { Name = name, Slug = $"col-{name}", Type = type };
|
||||
|
||||
[Fact]
|
||||
public async Task Resolves_across_a_rarity_gap()
|
||||
{
|
||||
// MilSpec + Classified present, Restricted absent → MilSpec must resolve to Classified.
|
||||
await using var db = NewContext();
|
||||
var cat = new Catalogue();
|
||||
var col = NewCollection("Gap");
|
||||
cat.Add(col, "Mil-Spec Grade");
|
||||
cat.Add(col, "Classified");
|
||||
db.Skins.AddRange(cat.Skins);
|
||||
|
||||
var graph = await BuildAsync(db);
|
||||
|
||||
var group = Assert.Single(graph.Groups);
|
||||
Assert.Equal(WeaponRarity.MilSpec, group.InputRarity);
|
||||
Assert.Equal(WeaponRarity.Classified, group.OutputRarity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Covert_is_an_output_but_never_an_input()
|
||||
{
|
||||
await using var db = NewContext();
|
||||
var cat = new Catalogue();
|
||||
var col = NewCollection("Tops at Covert");
|
||||
cat.Add(col, "Restricted");
|
||||
cat.Add(col, "Classified");
|
||||
cat.Add(col, "Covert"); // a weapon Covert (eligible output)
|
||||
db.Skins.AddRange(cat.Skins);
|
||||
|
||||
var graph = await BuildAsync(db);
|
||||
|
||||
Assert.Contains(graph.Groups, g => g.InputRarity == WeaponRarity.Restricted && g.OutputRarity == WeaponRarity.Classified);
|
||||
Assert.Contains(graph.Groups, g => g.InputRarity == WeaponRarity.Classified && g.OutputRarity == WeaponRarity.Covert);
|
||||
Assert.DoesNotContain(graph.Groups, g => g.InputRarity == WeaponRarity.Covert);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Knife_with_covert_rarity_is_excluded_as_output()
|
||||
{
|
||||
// The only "Covert" in the collection is a knife → the Covert tier has no eligible
|
||||
// output, so Classified resolves to nothing and yields no group.
|
||||
await using var db = NewContext();
|
||||
var cat = new Catalogue();
|
||||
var col = NewCollection("Knife Covert");
|
||||
cat.Add(col, "Classified");
|
||||
cat.Add(col, "Covert", weapon: cat.Knife);
|
||||
db.Skins.AddRange(cat.Skins);
|
||||
|
||||
var graph = await BuildAsync(db);
|
||||
|
||||
Assert.Empty(graph.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Contraband_and_gloves_are_not_weapon_tiers()
|
||||
{
|
||||
await using var db = NewContext();
|
||||
var cat = new Catalogue();
|
||||
var col = NewCollection("Howl");
|
||||
cat.Add(col, "Classified");
|
||||
cat.Add(col, "Contraband"); // The Howl
|
||||
cat.Add(col, "Extraordinary", weapon: cat.Glove); // a glove
|
||||
db.Skins.AddRange(cat.Skins);
|
||||
|
||||
var graph = await BuildAsync(db);
|
||||
|
||||
// Classified has no higher weapon tier present → no tradeup.
|
||||
Assert.Empty(graph.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Skins_without_float_bounds_are_excluded()
|
||||
{
|
||||
await using var db = NewContext();
|
||||
var cat = new Catalogue();
|
||||
var col = NewCollection("No Float");
|
||||
cat.Add(col, "Mil-Spec Grade", floatMin: null, floatMax: null); // floatless → not a tier
|
||||
cat.Add(col, "Classified");
|
||||
db.Skins.AddRange(cat.Skins);
|
||||
|
||||
var graph = await BuildAsync(db);
|
||||
|
||||
// The only would-be input tier is excluded, so nothing resolves.
|
||||
Assert.Empty(graph.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Limited_edition_pseudo_collection_is_skipped()
|
||||
{
|
||||
await using var db = NewContext();
|
||||
var cat = new Catalogue();
|
||||
var col = NewCollection("Limited Edition Item");
|
||||
cat.Add(col, "Mil-Spec Grade");
|
||||
cat.Add(col, "Classified");
|
||||
db.Skins.AddRange(cat.Skins);
|
||||
|
||||
var graph = await BuildAsync(db);
|
||||
|
||||
Assert.Empty(graph.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Containers_are_not_treated_as_collections()
|
||||
{
|
||||
await using var db = NewContext();
|
||||
var cat = new Catalogue();
|
||||
var crate = NewCollection("Some Case", type: "Container");
|
||||
cat.Add(crate, "Mil-Spec Grade");
|
||||
cat.Add(crate, "Classified");
|
||||
db.Skins.AddRange(cat.Skins);
|
||||
|
||||
var graph = await BuildAsync(db);
|
||||
|
||||
// Grouping is by Type='Collection' only; case weapons carry a separate Collection
|
||||
// source in reality, but a Container source on its own yields nothing.
|
||||
Assert.Empty(graph.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Output_skins_carry_float_bounds_and_stattrak_flag()
|
||||
{
|
||||
await using var db = NewContext();
|
||||
var cat = new Catalogue();
|
||||
var col = NewCollection("Bounds");
|
||||
cat.Add(col, "Classified");
|
||||
var covert = cat.Add(col, "Covert", floatMin: 0.0m, floatMax: 0.8m);
|
||||
covert.StatTrakAvailable = true;
|
||||
db.Skins.AddRange(cat.Skins);
|
||||
|
||||
var graph = await BuildAsync(db);
|
||||
|
||||
var group = Assert.Single(graph.Groups);
|
||||
var output = Assert.Single(group.OutputSkins);
|
||||
Assert.Equal(0.0m, output.FloatMin);
|
||||
Assert.Equal(0.8m, output.FloatMax);
|
||||
Assert.True(output.StatTrakAvailable);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user