ammocpr/test/ammoprices/scraping/retailers/natchez_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

110 lines
3.4 KiB
Elixir

defmodule Ammoprices.Scraping.Retailers.NatchezTest do
use ExUnit.Case, async: true
alias Ammoprices.Scraping.Retailers.Natchez
@fixture_path "test/fixtures/natchez/9mm.json"
describe "retailer_slug/0" do
test "returns natchez" do
assert Natchez.retailer_slug() == "natchez"
end
end
describe "category_url/1" do
test "returns nil for all calibers since fetch/2 is used" do
caliber = %{slug: "9mm-luger"}
assert Natchez.category_url(caliber) == nil
end
end
describe "build_query/1" do
test "builds GraphQL query for mapped caliber" do
query = Natchez.build_query("9mm-luger")
assert query =~ "9mm Luger"
assert query =~ "caliber"
assert query =~ "pageSize"
end
test "returns nil for unmapped caliber" do
assert Natchez.build_query("unknown-caliber") == nil
end
end
describe "parse_graphql_response/1" do
setup do
body = @fixture_path |> File.read!() |> Jason.decode!()
%{body: body}
end
test "extracts all products from response", %{body: body} do
products = Natchez.parse_graphql_response(body)
assert length(products) == 4
end
test "extracts product title from name", %{body: body} do
[first | _] = Natchez.parse_graphql_response(body)
assert first.title ==
"CCI Blazer Brass Handgun Ammunition 9mm Luger 115 gr FMJ 1145 fps 1000/ct Loose Bulk Pack"
end
test "extracts product URL from url_key", %{body: body} do
[first | _] = Natchez.parse_graphql_response(body)
assert first.url == "/cci-9mm-115gr-fmj-1000-rds-bulk-pack-cc5000bk1000.html"
end
test "extracts brand directly", %{body: body} do
[first | _] = Natchez.parse_graphql_response(body)
assert first.brand == "CCI"
end
test "extracts price in cents", %{body: body} do
[first | _] = Natchez.parse_graphql_response(body)
assert first.price_cents == 24_999
end
test "extracts grain weight from grain field", %{body: body} do
[first | _] = Natchez.parse_graphql_response(body)
# "115 gr" → 115
assert first.grain_weight == 115
end
test "extracts round count from rounds field", %{body: body} do
[first | _] = Natchez.parse_graphql_response(body)
assert first.round_count == 1000
end
test "calculates price per round", %{body: body} do
[first | _] = Natchez.parse_graphql_response(body)
# 24999 / 1000 = 24.999 → 25
assert first.price_per_round_cents == 25
end
test "extracts casing from case_material", %{body: body} do
products = Natchez.parse_graphql_response(body)
assert hd(products).casing == "brass"
# Third product: Steel case
assert Enum.at(products, 2).casing == "steel"
end
test "detects in_stock from stock_status", %{body: body} do
products = Natchez.parse_graphql_response(body)
# First: OUT_OF_STOCK
assert hd(products).in_stock == false
# Second: IN_STOCK
assert Enum.at(products, 1).in_stock == true
end
test "detects subsonic from name", %{body: body} do
products = Natchez.parse_graphql_response(body)
# Fourth product has "Subsonic" in name
assert Enum.at(products, 3).subsonic == true
end
test "non-subsonic products return false", %{body: body} do
[first | _] = Natchez.parse_graphql_response(body)
assert first.subsonic == false
end
end
end