|
|
@@ -43,11 +43,19 @@ func findGame(t *testing.T, games []game.Game, titlePrefix string) *game.Game {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-// Quirk 1 (regression): the ONLY reliable free indicator is the element's
|
|
|
-// price.totalPrice.discountPrice == 0. The offer's discountSetting.discountPrice
|
|
|
-// is null (→ 0) even for non-free offers. In the live fixture (Oct 2026),
|
|
|
-// Castlevania has offer.discountPrice == null but a real price of 7,99 € —
|
|
|
-// the old filter (offer.discountPrice == 0) wrongly reported it as free.
|
|
|
+// QUIRK — Epic's free indicator lives in the ELEMENT price, not the offer.
|
|
|
+// The API duplicates discount info in two places, and they disagree:
|
|
|
+//
|
|
|
+// - offer.promotions.promotionalOffers[].discountSetting.discountPrice
|
|
|
+// is NULL (→ 0 in Go) even for offers that are NOT free. Naively
|
|
|
+// treating 0 as "free" marked Castlevania (real price 7,99 €) as free —
|
|
|
+// exactly what the store does NOT show.
|
|
|
+// - The element's price.totalPrice.discountPrice is the actual current
|
|
|
+// price in cents and is the field the storefront itself renders.
|
|
|
+//
|
|
|
+// This test pins: free ⟺ element price == 0, judged against the live
|
|
|
+// fixture (Oct 2026: only "Sorry We're Closed" is free; Castlevania must
|
|
|
+// not appear despite discountSetting.discountPrice being null).
|
|
|
func TestLiveFixture_FreeIsDeterminedByElementPriceNotOfferDiscount(t *testing.T) {
|
|
|
c := serveFixture(t, "testdata/promotions_live.json")
|
|
|
games, err := c.GetFreeGames(context.Background())
|
|
|
@@ -68,9 +76,12 @@ func TestLiveFixture_FreeIsDeterminedByElementPriceNotOfferDiscount(t *testing.T
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// Quirk 2 (regression): discountPercentage in the offer's discountSetting is
|
|
|
-// NOT the discount in percent for free offers — a free offer can carry a
|
|
|
-// misleading value (e.g. 20). A free offer must be reported free regardless.
|
|
|
+// QUIRK — discountPercentage is meaningless for free offers. A genuinely
|
|
|
+// free element (price 0) can carry a promotionalOffers percentage of e.g.
|
|
|
+// 20 — nonsense as a "discount", just leftover campaign metadata. The old
|
|
|
+// filter (percentage != 100 → not free) would have dropped real free games.
|
|
|
+// Pinned: an element with price 0 and percentage 20 is reported FREE, and
|
|
|
+// without a DiscountPct (percent labels are only for real deals).
|
|
|
func TestFreeOfferWithMisleadingPercentageIsStillFree(t *testing.T) {
|
|
|
fixture := map[string]any{
|
|
|
"data": map[string]any{
|
|
|
@@ -97,9 +108,12 @@ func TestFreeOfferWithMisleadingPercentageIsStillFree(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// Quirk 3 (regression): discountPercentage in discountSetting is unreliable
|
|
|
-// for deals too (Castlevania: 40 while the store shows −60 %). Deals compute
|
|
|
-// the percent from the prices (original vs current, cents).
|
|
|
+// QUIRK — discountPercentage is inconsistent for deals too. Live example
|
|
|
+// (Oct 2026): Castlevania carries percentage 40 while the store page shows
|
|
|
+// −60 %. The real discount must be computed from the prices:
|
|
|
+// (originalPriceCents − discountPriceCents) / originalPriceCents. Pinned
|
|
|
+// with the live fixture: Castlevania 19,99 € → 7,99 € = −60 %, Eternal
|
|
|
+// Threads 17,99 € → 3,59 € = −80 %.
|
|
|
func TestLiveFixture_DealsComputePercentFromPrices(t *testing.T) {
|
|
|
c := serveFixture(t, "testdata/promotions_live.json")
|
|
|
deals, err := c.GetDeals(context.Background())
|
|
|
@@ -126,7 +140,12 @@ func TestLiveFixture_DealsComputePercentFromPrices(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// Quirk 4: free offers must never appear in the deals list and vice versa.
|
|
|
+// QUIRK — because discountSetting is unreliable (see above), a buggy filter
|
|
|
+// can classify the same element as free AND as deal, or drop it from both.
|
|
|
+// The lists are disjoint by construction (element price 0 = free, else
|
|
|
+// deal); this test guards that invariant against future filter changes.
|
|
|
+// The live fixture guarantees there ARE deals to check (Oct 2026: Eternal
|
|
|
+// Threads, Castlevania, Monument Valley as discounts).
|
|
|
func TestLiveFixture_FreeAndDealsAreDisjoint(t *testing.T) {
|
|
|
c := serveFixture(t, "testdata/promotions_live.json")
|
|
|
free, err := c.GetFreeGames(context.Background())
|
|
|
@@ -151,8 +170,11 @@ func TestLiveFixture_FreeAndDealsAreDisjoint(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// Quirk 5: offers without any price information (neither free nor deal) must
|
|
|
-// be skipped entirely (no phantom "free" entries).
|
|
|
+// QUIRK — some elements come with an active promotion window but WITHOUT a
|
|
|
+// price object at all. In Go, decoding that yields discountPrice == 0, which
|
|
|
+// naively reads as "free". But an element without price info is simply not
|
|
|
+// classifiable — the storefront never renders it as free either. Pinned:
|
|
|
+// missing price data must be skipped, never reported as free.
|
|
|
func TestOfferWithoutPriceFieldsIsSkipped(t *testing.T) {
|
|
|
fixture := map[string]any{
|
|
|
"data": map[string]any{
|
|
|
@@ -178,7 +200,12 @@ func TestOfferWithoutPriceFieldsIsSkipped(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// Upcoming windows come from upcomingPromotionalOffers and are sorted by start.
|
|
|
+// QUIRK — Epic announces future giveaways in a SEPARATE array
|
|
|
+// (upcomingPromotionalOffers), not in promotionalOffers. The two arrays use
|
|
|
+// the same offer shape but only upcoming ones may have a future startDate.
|
|
|
+// Also pinned: sorting by start date (soonest first) and kind = "upcoming"
|
|
|
+// — even when the element is currently free in another window (an element
|
|
|
+// can be free now AND have an upcoming window; both listings are valid).
|
|
|
func TestUpcomingGamesFromUpcomingWindows(t *testing.T) {
|
|
|
c := serveFixture(t, "testdata/promotions_live.json")
|
|
|
games, err := c.GetUpcomingGames(context.Background())
|
|
|
@@ -202,8 +229,10 @@ func TestUpcomingGamesFromUpcomingWindows(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// A game with several promotion windows (extended giveaway) is reported once
|
|
|
-// with the latest end date.
|
|
|
+// QUIRK — the same element can appear with MULTIPLE promotion windows (an
|
|
|
+// extended giveaway appends a second window with a later endDate). Without
|
|
|
+// dedup the game shows up twice in the list. Pinned: one entry per element
|
|
|
+// ID, carrying the LATEST end date.
|
|
|
func TestDedupWindowsKeepsLatestEndDate(t *testing.T) {
|
|
|
c := serveFixture(t, "testdata/promotions_live.json")
|
|
|
games, err := c.GetFreeGames(context.Background())
|
|
|
@@ -223,8 +252,12 @@ func TestDedupWindowsKeepsLatestEndDate(t *testing.T) {
|
|
|
|
|
|
// --- helpers ---
|
|
|
|
|
|
-// normTitle normalizes titles for comparisons: NBSP (\u00a0) → space. The API
|
|
|
-// returns titles containing NBSP (e.g. "Sorry We're Closed").
|
|
|
+// QUIRK — Epic titles contain NBSP (\u00a0) instead of regular spaces
|
|
|
+// (e.g. "Sorry We're Closed" with a non-breaking space after "Sorry").
|
|
|
+// Comparisons against plain-space literals silently fail; all title/price
|
|
|
+// comparisons in tests normalize NBSP first. This test file pins that the
|
|
|
+// fixtures really contain NBSP — if Epic ever switches to plain spaces,
|
|
|
+// normTitle stays harmless.
|
|
|
func normTitle(s string) string {
|
|
|
return strings.ReplaceAll(s, "\u00a0", " ")
|
|
|
}
|