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).
106 lines
3.5 KiB
Elixir
106 lines
3.5 KiB
Elixir
defmodule Ammoprices.Scraping.Retailers.PalmettoStateArmoryTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Ammoprices.Scraping.Retailers.PalmettoStateArmory
|
|
|
|
@fixture_path "test/fixtures/palmetto_state_armory/9mm.html"
|
|
|
|
describe "retailer_slug/0" do
|
|
test "returns palmetto-state-armory" do
|
|
assert PalmettoStateArmory.retailer_slug() == "palmetto-state-armory"
|
|
end
|
|
end
|
|
|
|
describe "category_url/1" do
|
|
test "maps 9mm-luger caliber to correct URL" do
|
|
caliber = %{slug: "9mm-luger"}
|
|
assert PalmettoStateArmory.category_url(caliber) == "/9mm-ammo.html"
|
|
end
|
|
|
|
test "maps 556-223 caliber to correct URL" do
|
|
caliber = %{slug: "556-223"}
|
|
assert PalmettoStateArmory.category_url(caliber) == "/223-remington-ammo.html"
|
|
end
|
|
|
|
test "returns nil for unmapped caliber" do
|
|
caliber = %{slug: "unknown-caliber"}
|
|
assert PalmettoStateArmory.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 = PalmettoStateArmory.parse_products(html)
|
|
assert length(products) == 4
|
|
end
|
|
|
|
test "extracts product title", %{html: html} do
|
|
[first | _] = PalmettoStateArmory.parse_products(html)
|
|
assert first.title == "TROY 9MM 115GR FMJ Ammo, 50rds - YTR9M115FMJ"
|
|
end
|
|
|
|
test "extracts product URL", %{html: html} do
|
|
[first | _] = PalmettoStateArmory.parse_products(html)
|
|
|
|
assert first.url ==
|
|
"https://palmettostatearmory.com/troy-9mm-115gr-fmj-ammo-50rds-ytr9m115fmj.html"
|
|
end
|
|
|
|
test "extracts price in cents from data-price-amount", %{html: html} do
|
|
[first | _] = PalmettoStateArmory.parse_products(html)
|
|
assert first.price_cents == 1199
|
|
end
|
|
|
|
test "extracts price per round in cents from unit_price", %{html: html} do
|
|
[first | _] = PalmettoStateArmory.parse_products(html)
|
|
# 0.2398 * 100 = 23.98 → 24 cents
|
|
assert first.price_per_round_cents == 24
|
|
end
|
|
|
|
test "extracts grain weight from title", %{html: html} do
|
|
[first | _] = PalmettoStateArmory.parse_products(html)
|
|
assert first.grain_weight == 115
|
|
end
|
|
|
|
test "extracts round count from title", %{html: html} do
|
|
[first | _] = PalmettoStateArmory.parse_products(html)
|
|
assert first.round_count == 50
|
|
end
|
|
|
|
test "marks all listed products as in stock", %{html: html} do
|
|
products = PalmettoStateArmory.parse_products(html)
|
|
assert Enum.all?(products, & &1.in_stock)
|
|
end
|
|
|
|
test "detects casing from title", %{html: html} do
|
|
products = PalmettoStateArmory.parse_products(html)
|
|
# Third product: "Steel Case" in title
|
|
assert Enum.at(products, 2).casing == "steel"
|
|
# Second product: "Brass" in title
|
|
assert Enum.at(products, 1).casing == "brass"
|
|
end
|
|
|
|
test "detects subsonic from title", %{html: html} do
|
|
products = PalmettoStateArmory.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 | _] = PalmettoStateArmory.parse_products(html)
|
|
assert first.subsonic == false
|
|
end
|
|
|
|
test "extracts brand from beginning of title", %{html: html} do
|
|
products = PalmettoStateArmory.parse_products(html)
|
|
assert hd(products).brand == "TROY"
|
|
assert Enum.at(products, 1).brand == "CCI"
|
|
assert Enum.at(products, 2).brand == "Tula"
|
|
end
|
|
end
|
|
end
|