206 lines
7.0 KiB
C#
206 lines
7.0 KiB
C#
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);
|
|
}
|
|
}
|