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).
115 lines
3.6 KiB
Elixir
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
|