Phoenix 1.8 app that scrapes ammunition retailers (Lucky Gunner, SGAmmo) for price data and displays historical price trends. - Data model: retailers, calibers, products, price snapshots - Scraper infrastructure with Req, Floki, realistic browser headers - Oban-scheduled scrape jobs (every 4h with randomized delays) - LiveView pages: homepage with category cards, caliber detail with price table, Chart.js price history, and price stats banner - 18 seeded calibers across handgun/rifle/rimfire/shotgun categories - 77 tests
155 lines
5.2 KiB
Elixir
155 lines
5.2 KiB
Elixir
defmodule Ammoprices.CatalogTest do
|
|
use Ammoprices.DataCase, async: true
|
|
|
|
import Ammoprices.Fixtures
|
|
|
|
alias Ammoprices.Catalog
|
|
|
|
describe "retailers" do
|
|
test "list_retailers/0 returns all retailers" do
|
|
retailer = retailer_fixture()
|
|
assert Catalog.list_retailers() == [retailer]
|
|
end
|
|
|
|
test "get_retailer!/1 returns the retailer with given id" do
|
|
retailer = retailer_fixture()
|
|
assert Catalog.get_retailer!(retailer.id) == retailer
|
|
end
|
|
|
|
test "get_retailer_by_slug!/1 returns the retailer with given slug" do
|
|
retailer = retailer_fixture(%{slug: "lucky-gunner"})
|
|
assert Catalog.get_retailer_by_slug!("lucky-gunner") == retailer
|
|
end
|
|
|
|
test "create_retailer/1 with valid data creates a retailer" do
|
|
attrs = %{name: "Lucky Gunner", slug: "lucky-gunner", base_url: "https://luckygunner.com"}
|
|
assert {:ok, retailer} = Catalog.create_retailer(attrs)
|
|
assert retailer.name == "Lucky Gunner"
|
|
assert retailer.slug == "lucky-gunner"
|
|
assert retailer.base_url == "https://luckygunner.com"
|
|
assert retailer.enabled == true
|
|
end
|
|
|
|
test "create_retailer/1 with duplicate slug returns error" do
|
|
retailer_fixture(%{slug: "duplicate"})
|
|
attrs = %{name: "Another", slug: "duplicate", base_url: "https://example.com"}
|
|
assert {:error, changeset} = Catalog.create_retailer(attrs)
|
|
assert %{slug: ["has already been taken"]} = errors_on(changeset)
|
|
end
|
|
|
|
test "create_retailer/1 with missing required fields returns error" do
|
|
assert {:error, changeset} = Catalog.create_retailer(%{})
|
|
assert %{name: ["can't be blank"]} = errors_on(changeset)
|
|
end
|
|
|
|
test "update_retailer/2 updates the retailer" do
|
|
retailer = retailer_fixture()
|
|
assert {:ok, updated} = Catalog.update_retailer(retailer, %{name: "Updated Name"})
|
|
assert updated.name == "Updated Name"
|
|
end
|
|
end
|
|
|
|
describe "calibers" do
|
|
test "list_calibers/0 returns all calibers" do
|
|
caliber = caliber_fixture()
|
|
assert Catalog.list_calibers() == [caliber]
|
|
end
|
|
|
|
test "list_calibers_by_category/1 filters by category" do
|
|
handgun = caliber_fixture(%{name: "9mm", slug: "9mm", category: "handgun"})
|
|
_rifle = caliber_fixture(%{name: "5.56", slug: "556", category: "rifle"})
|
|
|
|
result = Catalog.list_calibers_by_category("handgun")
|
|
assert result == [handgun]
|
|
end
|
|
|
|
test "get_caliber!/1 returns the caliber with given id" do
|
|
caliber = caliber_fixture()
|
|
assert Catalog.get_caliber!(caliber.id) == caliber
|
|
end
|
|
|
|
test "get_caliber_by_slug!/1 returns the caliber with given slug" do
|
|
caliber = caliber_fixture(%{slug: "9mm-luger"})
|
|
assert Catalog.get_caliber_by_slug!("9mm-luger") == caliber
|
|
end
|
|
|
|
test "create_caliber/1 with valid data creates a caliber" do
|
|
attrs = %{
|
|
name: ".45 ACP",
|
|
slug: "45-acp",
|
|
category: "handgun",
|
|
aliases: [".45", "45 auto"]
|
|
}
|
|
|
|
assert {:ok, caliber} = Catalog.create_caliber(attrs)
|
|
assert caliber.name == ".45 ACP"
|
|
assert caliber.aliases == [".45", "45 auto"]
|
|
end
|
|
|
|
test "create_caliber/1 with invalid category returns error" do
|
|
attrs = %{name: "Test", slug: "test", category: "invalid"}
|
|
assert {:error, changeset} = Catalog.create_caliber(attrs)
|
|
assert %{category: ["is invalid"]} = errors_on(changeset)
|
|
end
|
|
end
|
|
|
|
describe "products" do
|
|
test "upsert_product/3 creates a new product" do
|
|
retailer = retailer_fixture()
|
|
caliber = caliber_fixture()
|
|
|
|
attrs = %{
|
|
title: "Federal 9mm 115gr FMJ",
|
|
url: "/products/federal-9mm",
|
|
brand: "Federal",
|
|
grain_weight: 115,
|
|
round_count: 50,
|
|
casing: "brass",
|
|
condition: "new",
|
|
in_stock: true,
|
|
last_seen_at: DateTime.truncate(DateTime.utc_now(), :second)
|
|
}
|
|
|
|
assert {:ok, product} = Catalog.upsert_product(retailer.id, caliber.id, attrs)
|
|
assert product.title == "Federal 9mm 115gr FMJ"
|
|
assert product.retailer_id == retailer.id
|
|
assert product.caliber_id == caliber.id
|
|
end
|
|
|
|
test "upsert_product/3 updates existing product on duplicate retailer_id + url" do
|
|
retailer = retailer_fixture()
|
|
caliber = caliber_fixture()
|
|
|
|
attrs = %{
|
|
title: "Federal 9mm 115gr FMJ",
|
|
url: "/products/federal-9mm",
|
|
brand: "Federal",
|
|
grain_weight: 115,
|
|
round_count: 50,
|
|
casing: "brass",
|
|
in_stock: true,
|
|
last_seen_at: DateTime.truncate(DateTime.utc_now(), :second)
|
|
}
|
|
|
|
assert {:ok, product1} = Catalog.upsert_product(retailer.id, caliber.id, attrs)
|
|
|
|
assert {:ok, product2} =
|
|
Catalog.upsert_product(retailer.id, caliber.id, %{attrs | title: "Updated Title", in_stock: false})
|
|
|
|
assert product1.id == product2.id
|
|
assert product2.title == "Updated Title"
|
|
assert product2.in_stock == false
|
|
end
|
|
|
|
test "list_products_for_caliber/2 returns products for a caliber" do
|
|
caliber = caliber_fixture()
|
|
retailer = retailer_fixture()
|
|
product = product_fixture(%{caliber: caliber, retailer: retailer})
|
|
_other = product_fixture()
|
|
|
|
results = Catalog.list_products_for_caliber(caliber.id)
|
|
assert length(results) == 1
|
|
assert hd(results).id == product.id
|
|
end
|
|
end
|
|
end
|