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).
110 lines
3.4 KiB
Elixir
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
|