ammocpr/test/ammoprices/scraping/retailers/lucky_gunner_test.exs
Graham McIntire 16a28ac01c
Add Target Sports USA scraper, product filtering, and subsonic detection
- Add subsonic boolean field to products with composite filter indexes
- Add TextDetector utility for detecting subsonic/casing from product titles
- Wire TextDetector into SgAmmo (casing + subsonic) and LuckyGunner (subsonic + fallback casing)
- Extend latest_prices_for_caliber with casing, grain_weight, and subsonic filter options
- Add distinct_grain_weights_for_caliber and distinct_casings_for_caliber queries
- Add filter UI with toggleable casing, grain weight, and subsonic buttons
- Add grain weight column and subsonic badge to product table
- Refresh filter options on PubSub price updates for real-time UI
- Implement Target Sports USA scraper with 18 caliber mappings
- Display all prices in dollar format ($0.38 instead of 38¢)
2026-03-11 16:28:04 -05:00

109 lines
3.5 KiB
Elixir

defmodule Ammoprices.Scraping.Retailers.LuckyGunnerTest do
use ExUnit.Case, async: true
alias Ammoprices.Scraping.Retailers.LuckyGunner
@fixture_path "test/fixtures/lucky_gunner/9mm.html"
describe "retailer_slug/0" do
test "returns lucky-gunner" do
assert LuckyGunner.retailer_slug() == "lucky-gunner"
end
end
describe "category_url/1" do
test "maps 9mm-luger caliber to correct URL" do
caliber = %{slug: "9mm-luger", category: "handgun"}
assert LuckyGunner.category_url(caliber) == "/handgun/9mm-ammo"
end
test "returns nil for unmapped caliber" do
caliber = %{slug: "unknown-caliber", category: "handgun"}
assert LuckyGunner.category_url(caliber) == nil
end
end
describe "parse_products/1" do
setup do
html = File.read!(@fixture_path)
%{html: html}
end
test "extracts all products from HTML", %{html: html} do
products = LuckyGunner.parse_products(html)
assert length(products) == 4
end
test "extracts product title", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.title == "9mm - 115 Grain FMJ - Wolf - 1350 Rounds **STEEL CASES**"
end
test "extracts product URL", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.url == "https://www.luckygunner.com/9mm-115-grain-fmj-wolf-1350-rounds"
end
test "extracts price in cents", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.price_cents == 27_000
end
test "extracts price per round in cents", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.price_per_round_cents == 20
end
test "extracts fractional price per round", %{html: html} do
products = LuckyGunner.parse_products(html)
second = Enum.at(products, 1)
# 21.2¢ → 21 cents (truncated to integer)
assert second.price_per_round_cents == 21
end
test "extracts brand from description", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.brand == "Wolf"
end
test "extracts grain weight from description", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.grain_weight == 115
end
test "extracts round count from title", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.round_count == 1350
end
test "detects casing material from description", %{html: html} do
products = LuckyGunner.parse_products(html)
# First product: steel casings
assert hd(products).casing == "steel"
# Third product: brass casings
assert Enum.at(products, 2).casing == "brass"
end
test "marks products as in stock", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.in_stock == true
end
test "detects subsonic from title", %{html: html} do
products = LuckyGunner.parse_products(html)
# 4th product has "Subsonic" in title
assert Enum.at(products, 3).subsonic == true
end
test "non-subsonic products return false", %{html: html} do
[first | _] = LuckyGunner.parse_products(html)
assert first.subsonic == false
end
test "detects casing from description with TextDetector fallback", %{html: html} do
products = LuckyGunner.parse_products(html)
# 4th product: nickel-plated brass from description
assert Enum.at(products, 3).casing == "brass"
end
end
end