ammocpr/test/ammoprices/scraping/retailers/bulk_ammo_test.exs
Graham McIntire 9eb85faebd
Add 4 new retailer scrapers for broader price coverage
Add True Shot Ammo (Shopify JSON), Palmetto State Armory (Magento HTML),
Bulk Ammo (Magento HTML), and Natchez Shooters Supply (GraphQL API).

Extends scraper infrastructure with HttpClient.post_json/3 and an
optional fetch/2 callback on the Scraper behaviour for scrapers that
need custom HTTP flows (e.g. GraphQL POST requests).
2026-03-12 10:00:59 -05:00

115 lines
3.6 KiB
Elixir

defmodule Ammoprices.Scraping.Retailers.BulkAmmoTest do
use ExUnit.Case, async: true
alias Ammoprices.Scraping.Retailers.BulkAmmo
@fixture_path "test/fixtures/bulk_ammo/9mm.html"
describe "retailer_slug/0" do
test "returns bulk-ammo" do
assert BulkAmmo.retailer_slug() == "bulk-ammo"
end
end
describe "category_url/1" do
test "maps 9mm-luger caliber to correct URL" do
caliber = %{slug: "9mm-luger"}
assert BulkAmmo.category_url(caliber) == "/handgun/bulk-9mm-ammo"
end
test "maps 556-223 caliber to correct URL" do
caliber = %{slug: "556-223"}
assert BulkAmmo.category_url(caliber) == "/rifle/bulk-223-remington-ammo"
end
test "returns nil for unmapped caliber" do
caliber = %{slug: "unknown-caliber"}
assert BulkAmmo.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 = BulkAmmo.parse_products(html)
assert length(products) == 4
end
test "extracts product title", %{html: html} do
[first | _] = BulkAmmo.parse_products(html)
assert first.title == "1500 Rounds of 9mm Ammo by Sterling Steel - 115gr FMJ"
end
test "extracts product URL", %{html: html} do
[first | _] = BulkAmmo.parse_products(html)
assert first.url == "https://www.bulkammo.com/1500-rounds-of-9mm-ammo-by-sterling-115gr-fmj"
end
test "extracts sale price when present", %{html: html} do
[first | _] = BulkAmmo.parse_products(html)
# $285 sale price
assert first.price_cents == 28_500
end
test "extracts regular price when no sale", %{html: html} do
products = BulkAmmo.parse_products(html)
second = Enum.at(products, 1)
# $239.99 regular price
assert second.price_cents == 23_999
end
test "extracts round count from title", %{html: html} do
[first | _] = BulkAmmo.parse_products(html)
assert first.round_count == 1500
end
test "calculates price per round", %{html: html} do
[first | _] = BulkAmmo.parse_products(html)
# 28500 / 1500 = 19
assert first.price_per_round_cents == 19
end
test "extracts brand from title", %{html: html} do
products = BulkAmmo.parse_products(html)
assert hd(products).brand == "Sterling Steel"
assert Enum.at(products, 1).brand == "Remington"
assert Enum.at(products, 2).brand == "Federal Brass"
end
test "extracts grain weight from title", %{html: html} do
[first | _] = BulkAmmo.parse_products(html)
assert first.grain_weight == 115
end
test "detects in-stock status", %{html: html} do
products = BulkAmmo.parse_products(html)
assert hd(products).in_stock == true
assert Enum.at(products, 1).in_stock == true
# Fourth product is out of stock
assert Enum.at(products, 3).in_stock == false
end
test "detects casing from title", %{html: html} do
products = BulkAmmo.parse_products(html)
# First product: "Sterling Steel" in title
assert hd(products).casing == "steel"
# Third product: "Federal Brass" in title
assert Enum.at(products, 2).casing == "brass"
end
test "detects subsonic from title", %{html: html} do
products = BulkAmmo.parse_products(html)
# Fourth product: "Subsonic" in title
assert Enum.at(products, 3).subsonic == true
end
test "non-subsonic products return false", %{html: html} do
[first | _] = BulkAmmo.parse_products(html)
assert first.subsonic == false
end
end
end