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);
|
||||
}
|
||||
}
|
||||
52
BlueLaminate/BlueLaminate.Tests/Tradeups/TradeupMathTests.cs
Normal file
52
BlueLaminate/BlueLaminate.Tests/Tradeups/TradeupMathTests.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using BlueLaminate.Core.Tradeups;
|
||||
using Xunit;
|
||||
|
||||
namespace BlueLaminate.Tests.Tradeups;
|
||||
|
||||
public class TradeupMathTests
|
||||
{
|
||||
[Fact]
|
||||
public void NormalizedFraction_maps_value_into_its_own_range()
|
||||
{
|
||||
// Mid-point of a 0.06–0.80 range.
|
||||
var frac = TradeupMath.NormalizedFraction(0.43m, 0.06m, 0.80m);
|
||||
Assert.Equal(0.5m, frac, precision: 6);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-0.5)] // below min
|
||||
[InlineData(2.0)] // above max
|
||||
public void NormalizedFraction_clamps_out_of_range_values(double value)
|
||||
{
|
||||
var frac = TradeupMath.NormalizedFraction((decimal)value, 0.0m, 1.0m);
|
||||
Assert.InRange(frac, 0m, 1m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizedFraction_returns_zero_for_zero_width_range()
|
||||
{
|
||||
Assert.Equal(0m, TradeupMath.NormalizedFraction(0.3m, 0.3m, 0.3m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputFloat_maps_average_fraction_onto_output_range()
|
||||
{
|
||||
// avg 0.10 onto a 0.00–0.70 output range → 0.07 (the FN/MW boundary).
|
||||
var outFloat = TradeupMath.OutputFloat(0.10m, 0.00m, 0.70m);
|
||||
Assert.Equal(0.07m, outFloat, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Full_contract_float_math_matches_hand_calculation()
|
||||
{
|
||||
// Ten inputs, each normalised to its own range, then averaged and mapped onto the
|
||||
// output's 0.00–0.80 range. Five inputs at fraction 0.2 and five at 0.4 → avg 0.3.
|
||||
var fractions = new[] { 0.2m, 0.2m, 0.2m, 0.2m, 0.2m, 0.4m, 0.4m, 0.4m, 0.4m, 0.4m };
|
||||
var avg = fractions.Sum() / fractions.Length;
|
||||
Assert.Equal(0.3m, avg, precision: 6);
|
||||
|
||||
var outFloat = TradeupMath.OutputFloat(avg, 0.00m, 0.80m);
|
||||
Assert.Equal(0.24m, outFloat, precision: 6);
|
||||
Assert.Equal(WearBand.FieldTested, WearBands.FromFloat(outFloat));
|
||||
}
|
||||
}
|
||||
204
BlueLaminate/BlueLaminate.Tests/Tradeups/TradeupSelectorTests.cs
Normal file
204
BlueLaminate/BlueLaminate.Tests/Tradeups/TradeupSelectorTests.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using BlueLaminate.Core.Tradeups;
|
||||
using Xunit;
|
||||
|
||||
namespace BlueLaminate.Tests.Tradeups;
|
||||
|
||||
public class TradeupSelectorTests
|
||||
{
|
||||
private const decimal Bucket = 0.005m;
|
||||
|
||||
private static SelectableInput Item(decimal fraction, decimal price)
|
||||
=> new(fraction, new InputListing(
|
||||
SkinId: 1,
|
||||
MarketHashName: "Test Skin",
|
||||
Marketplace: "test",
|
||||
InspectLink: null,
|
||||
ExternalId: "0",
|
||||
FloatValue: fraction,
|
||||
Price: price));
|
||||
|
||||
[Fact]
|
||||
public void Cheapest_full_selection_is_the_ten_cheapest_copies()
|
||||
{
|
||||
// 12 copies priced 1..12 at assorted fractions. With no binding float target the
|
||||
// cheapest ten (cost 55) must be attainable at some summed-fraction bucket.
|
||||
var pool = new List<SelectableInput>();
|
||||
for (var i = 1; i <= 12; i++)
|
||||
{
|
||||
pool.Add(Item(fraction: 0.01m * i, price: i));
|
||||
}
|
||||
|
||||
var selection = TradeupSelector.Solve(pool, contractSize: 10, Bucket);
|
||||
var selections = selection.Selections().ToList();
|
||||
|
||||
Assert.NotEmpty(selections);
|
||||
|
||||
var cheapest = selections.MinBy(s => s.Cost);
|
||||
Assert.Equal(55m, cheapest.Cost);
|
||||
|
||||
var picks = cheapest.Picks.ToList();
|
||||
Assert.Equal(10, picks.Count);
|
||||
Assert.Equal(Enumerable.Range(1, 10).Select(i => (decimal)i), picks.Select(p => p.Price).OrderBy(p => p));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reported_average_fraction_is_a_conservative_upper_bound()
|
||||
{
|
||||
var pool = new List<SelectableInput>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
pool.Add(Item(fraction: 0.123m, price: 1m));
|
||||
}
|
||||
|
||||
var only = Assert.Single(TradeupSelector.Solve(pool, 10, Bucket).Selections());
|
||||
|
||||
var trueAverage = only.Picks.ToList().Average(p => p.FloatValue);
|
||||
Assert.True(only.AverageFraction >= trueAverage,
|
||||
$"bucketed average {only.AverageFraction} should round up from true {trueAverage}");
|
||||
// 0.123 rounds up to the 0.125 bucket (0.005 grid).
|
||||
Assert.Equal(0.125m, only.AverageFraction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selecting_cheaper_low_float_copies_lowers_the_attainable_average()
|
||||
{
|
||||
// Cheap high-float copies vs. pricier low-float copies. A lower average is only
|
||||
// reachable by paying for the low-float set, so a low-average selection must cost
|
||||
// more than the unconstrained cheapest.
|
||||
var pool = new List<SelectableInput>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
pool.Add(Item(fraction: 0.60m, price: 1m)); // cheap, bad float
|
||||
}
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
pool.Add(Item(fraction: 0.10m, price: 5m)); // pricey, good float
|
||||
}
|
||||
|
||||
var selections = TradeupSelector.Solve(pool, 10, Bucket).Selections().ToList();
|
||||
|
||||
var cheapest = selections.MinBy(s => s.Cost);
|
||||
Assert.Equal(10m, cheapest.Cost); // ten cheap copies
|
||||
Assert.Equal(0.60m, cheapest.AverageFraction);
|
||||
|
||||
var lowestFloat = selections.MinBy(s => s.AverageFraction);
|
||||
Assert.Equal(0.10m, lowestFloat.AverageFraction);
|
||||
Assert.Equal(50m, lowestFloat.Cost); // forced onto the pricey low-float set
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void No_full_selection_when_pool_is_too_small()
|
||||
{
|
||||
var pool = Enumerable.Range(0, 5).Select(i => Item(0.2m, i + 1)).ToList();
|
||||
Assert.Empty(TradeupSelector.Solve(pool, 10, Bucket).Selections());
|
||||
}
|
||||
|
||||
private static TradeupSelector.RewardItem Reward(int bucket, double reward, decimal price = 0m)
|
||||
=> new(bucket, reward, new InputListing(1, "x", "m", null, "0", 0.1m, price));
|
||||
|
||||
[Fact]
|
||||
public void MaxReward_picks_the_highest_reward_set_within_the_cap()
|
||||
{
|
||||
// Six items; pick 3 maximising reward with bucket sum ≤ 6. The three best rewards
|
||||
// (9, 8, 7) sit at buckets 2,2,2 = 6 ≤ cap, so they win.
|
||||
var items = new[]
|
||||
{
|
||||
Reward(2, 9), Reward(2, 8), Reward(2, 7),
|
||||
Reward(1, 1), Reward(1, 2), Reward(1, 3),
|
||||
};
|
||||
|
||||
var picks = TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 6);
|
||||
|
||||
Assert.NotNull(picks);
|
||||
Assert.Equal(3, picks!.ToList().Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxReward_respects_the_float_cap_even_at_the_cost_of_reward()
|
||||
{
|
||||
// The fat-reward items sit at high buckets; the cap forces the low-bucket set.
|
||||
var items = new[]
|
||||
{
|
||||
Reward(5, 100, price: 50m), // too high-float to fit under a tight cap
|
||||
Reward(1, 5, price: 1m),
|
||||
Reward(1, 4, price: 1m),
|
||||
Reward(1, 3, price: 1m),
|
||||
};
|
||||
|
||||
var picks = TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 3);
|
||||
|
||||
Assert.NotNull(picks);
|
||||
var chosen = picks!.ToList();
|
||||
Assert.Equal(3, chosen.Count);
|
||||
Assert.All(chosen, p => Assert.Equal(1m, p.Price)); // the three low-float copies
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxReward_returns_null_when_the_contract_cannot_be_filled_under_the_cap()
|
||||
{
|
||||
// Only two items fit under the cap, but three are required.
|
||||
var items = new[] { Reward(1, 5), Reward(1, 4), Reward(9, 100) };
|
||||
Assert.Null(TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 3));
|
||||
}
|
||||
}
|
||||
|
||||
public class OutputPriceBookTests
|
||||
{
|
||||
private static TradeupListingRow Row(WearBand band, bool st, decimal price)
|
||||
{
|
||||
// A float squarely inside the band, so Build files it where we expect.
|
||||
var f = band switch
|
||||
{
|
||||
WearBand.FactoryNew => 0.03m,
|
||||
WearBand.MinimalWear => 0.10m,
|
||||
WearBand.FieldTested => 0.25m,
|
||||
WearBand.WellWorn => 0.40m,
|
||||
_ => 0.60m,
|
||||
};
|
||||
return new TradeupListingRow(1, "M4A4 | X-Ray", "csmoney", null, "0", st, false, f, price);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Liquid_band_uses_its_own_price()
|
||||
{
|
||||
// Ten MW listings -> the MW band is trusted at its own lowest ask.
|
||||
var rows = Enumerable.Range(0, 10).Select(i => Row(WearBand.MinimalWear, true, 90m + i));
|
||||
var book = TradeupListingData.Build(rows).OutputPrices;
|
||||
|
||||
var r = book.Resolve(1, statTrak: true, outputFloat: 0.10m, thinThreshold: 10);
|
||||
|
||||
Assert.Equal(OutputPriceBasis.Band, r.Basis);
|
||||
Assert.Equal(90m, r.LowestAsk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Thin_band_falls_back_to_the_skin_overall_floor()
|
||||
{
|
||||
// The X-Ray case: two outlier FN listings ($1287) but a liquid MW market (~$90).
|
||||
// A produced FN output must price off the $90 floor, not the $1287 band outlier.
|
||||
var rows = new List<TradeupListingRow>
|
||||
{
|
||||
Row(WearBand.FactoryNew, true, 1287m),
|
||||
Row(WearBand.FactoryNew, true, 1290m),
|
||||
};
|
||||
rows.AddRange(Enumerable.Range(0, 10).Select(i => Row(WearBand.MinimalWear, true, 90m + i)));
|
||||
|
||||
var book = TradeupListingData.Build(rows).OutputPrices;
|
||||
|
||||
var r = book.Resolve(1, statTrak: true, outputFloat: 0.0695m, thinThreshold: 10);
|
||||
|
||||
Assert.Equal(OutputPriceBasis.Floor, r.Basis);
|
||||
Assert.Equal(90m, r.LowestAsk); // skin-wide floor, not the $1287 FN outlier
|
||||
Assert.Equal(2, r.BandLiquidity); // still reports the thin band count → triggers CSFloat
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unlisted_skin_resolves_to_none()
|
||||
{
|
||||
var book = TradeupListingData.Build(Array.Empty<TradeupListingRow>()).OutputPrices;
|
||||
var r = book.Resolve(1, statTrak: false, outputFloat: 0.1m, thinThreshold: 10);
|
||||
Assert.Equal(OutputPriceBasis.None, r.Basis);
|
||||
Assert.Null(r.LowestAsk);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using BlueLaminate.Core.Tradeups;
|
||||
using Xunit;
|
||||
|
||||
namespace BlueLaminate.Tests.Tradeups;
|
||||
|
||||
public class WeaponRarityTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Consumer Grade", WeaponRarity.Consumer)]
|
||||
[InlineData("Industrial Grade", WeaponRarity.Industrial)]
|
||||
[InlineData("Mil-Spec Grade", WeaponRarity.MilSpec)]
|
||||
[InlineData("Restricted", WeaponRarity.Restricted)]
|
||||
[InlineData("Classified", WeaponRarity.Classified)]
|
||||
[InlineData("Covert", WeaponRarity.Covert)]
|
||||
public void Maps_each_weapon_tier_literal(string literal, WeaponRarity expected)
|
||||
{
|
||||
Assert.True(WeaponRarityExtensions.TryParse(literal, out var rarity));
|
||||
Assert.Equal(expected, rarity);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Contraband")] // The Howl
|
||||
[InlineData("Extraordinary")] // Gloves
|
||||
public void Reports_non_weapon_rarities_as_not_a_tier(string literal)
|
||||
{
|
||||
Assert.False(WeaponRarityExtensions.TryParse(literal, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Throws_on_unknown_literal_so_a_catalogue_rename_is_loud()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => WeaponRarityExtensions.TryParse("Mythical", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tiers_are_strictly_ordered()
|
||||
{
|
||||
Assert.True(WeaponRarity.Consumer < WeaponRarity.Industrial);
|
||||
Assert.True(WeaponRarity.MilSpec < WeaponRarity.Restricted);
|
||||
Assert.True(WeaponRarity.Classified < WeaponRarity.Covert);
|
||||
}
|
||||
}
|
||||
31
BlueLaminate/BlueLaminate.Tests/Tradeups/WearBandTests.cs
Normal file
31
BlueLaminate/BlueLaminate.Tests/Tradeups/WearBandTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using BlueLaminate.Core.Tradeups;
|
||||
using Xunit;
|
||||
|
||||
namespace BlueLaminate.Tests.Tradeups;
|
||||
|
||||
public class WearBandTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0.00, WearBand.FactoryNew)]
|
||||
[InlineData(0.0699, WearBand.FactoryNew)]
|
||||
[InlineData(0.07, WearBand.MinimalWear)] // boundary is upper-exclusive
|
||||
[InlineData(0.1499, WearBand.MinimalWear)]
|
||||
[InlineData(0.15, WearBand.FieldTested)]
|
||||
[InlineData(0.3799, WearBand.FieldTested)]
|
||||
[InlineData(0.38, WearBand.WellWorn)]
|
||||
[InlineData(0.4499, WearBand.WellWorn)]
|
||||
[InlineData(0.45, WearBand.BattleScarred)]
|
||||
[InlineData(1.00, WearBand.BattleScarred)]
|
||||
public void FromFloat_classifies_on_absolute_thresholds(double value, WearBand expected)
|
||||
{
|
||||
Assert.Equal(expected, WearBands.FromFloat((decimal)value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToName_matches_listing_wear_strings()
|
||||
{
|
||||
Assert.Equal("Factory New", WearBand.FactoryNew.ToName());
|
||||
Assert.Equal("Field-Tested", WearBand.FieldTested.ToName());
|
||||
Assert.Equal("Battle-Scarred", WearBand.BattleScarred.ToName());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user