2026-02-18 21:23:15 +00:00
|
|
|
defmodule Berrypod.ProductsUpsertTest do
|
|
|
|
|
use Berrypod.DataCase, async: false
|
2026-01-31 22:08:34 +00:00
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
alias Berrypod.Products
|
2026-01-31 22:08:34 +00:00
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
import Berrypod.ProductsFixtures
|
2026-01-31 22:08:34 +00:00
|
|
|
|
|
|
|
|
describe "upsert_product/2" do
|
|
|
|
|
test "creates a new product when it doesn't exist" do
|
|
|
|
|
conn = provider_connection_fixture()
|
|
|
|
|
|
|
|
|
|
attrs = %{
|
|
|
|
|
provider_product_id: "new-product-123",
|
|
|
|
|
title: "New Product",
|
|
|
|
|
description: "A new product",
|
|
|
|
|
provider_data: %{"blueprint_id" => 145}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert {:ok, product, :created} = Products.upsert_product(conn, attrs)
|
|
|
|
|
assert product.title == "New Product"
|
|
|
|
|
assert product.provider_product_id == "new-product-123"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "returns unchanged when checksum matches" do
|
|
|
|
|
conn = provider_connection_fixture()
|
|
|
|
|
provider_data = %{"blueprint_id" => 145, "data" => "same"}
|
|
|
|
|
|
|
|
|
|
{:ok, original, :created} =
|
|
|
|
|
Products.upsert_product(conn, %{
|
|
|
|
|
provider_product_id: "product-123",
|
|
|
|
|
title: "Product",
|
|
|
|
|
provider_data: provider_data
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Same provider_data = same checksum
|
|
|
|
|
{:ok, product, :unchanged} =
|
|
|
|
|
Products.upsert_product(conn, %{
|
|
|
|
|
provider_product_id: "product-123",
|
|
|
|
|
title: "Product Updated",
|
|
|
|
|
provider_data: provider_data
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert product.id == original.id
|
|
|
|
|
# Title should NOT be updated since checksum matched
|
|
|
|
|
assert product.title == "Product"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "updates product when checksum differs" do
|
|
|
|
|
conn = provider_connection_fixture()
|
|
|
|
|
|
|
|
|
|
{:ok, original, :created} =
|
|
|
|
|
Products.upsert_product(conn, %{
|
|
|
|
|
provider_product_id: "product-123",
|
|
|
|
|
title: "Product",
|
|
|
|
|
provider_data: %{"version" => 1}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Different provider_data = different checksum
|
|
|
|
|
{:ok, product, :updated} =
|
|
|
|
|
Products.upsert_product(conn, %{
|
|
|
|
|
provider_product_id: "product-123",
|
|
|
|
|
title: "Product Updated",
|
|
|
|
|
provider_data: %{"version" => 2}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert product.id == original.id
|
|
|
|
|
assert product.title == "Product Updated"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "handles race condition when two processes try to insert same product" do
|
|
|
|
|
conn = provider_connection_fixture()
|
|
|
|
|
provider_product_id = "race-condition-test-#{System.unique_integer()}"
|
|
|
|
|
|
|
|
|
|
attrs = %{
|
|
|
|
|
provider_product_id: provider_product_id,
|
|
|
|
|
title: "Race Condition Product",
|
|
|
|
|
provider_data: %{"test" => true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Simulate race condition by running concurrent inserts
|
|
|
|
|
tasks =
|
|
|
|
|
for _ <- 1..5 do
|
|
|
|
|
Task.async(fn ->
|
|
|
|
|
Products.upsert_product(conn, attrs)
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
results = Task.await_many(tasks, 5000)
|
|
|
|
|
|
|
|
|
|
# All should succeed (no crashes)
|
|
|
|
|
assert Enum.all?(results, fn
|
|
|
|
|
{:ok, _product, status} when status in [:created, :unchanged] -> true
|
|
|
|
|
_ -> false
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
# Only one product should exist
|
|
|
|
|
assert Products.count_products_for_connection(conn.id) >= 1
|
|
|
|
|
|
|
|
|
|
# Verify we can fetch the product
|
|
|
|
|
product = Products.get_product_by_provider(conn.id, provider_product_id)
|
|
|
|
|
assert product.title == "Race Condition Product"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "matches by slug when provider_product_id changes" do
|
|
|
|
|
conn = provider_connection_fixture()
|
|
|
|
|
|
|
|
|
|
# Create first product
|
|
|
|
|
{:ok, product1, :created} =
|
|
|
|
|
Products.upsert_product(conn, %{
|
|
|
|
|
provider_product_id: "old-product-id",
|
|
|
|
|
title: "Same Title Product",
|
|
|
|
|
provider_data: %{"id" => 1}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert product1.provider_product_id == "old-product-id"
|
|
|
|
|
|
|
|
|
|
# Same title but different provider_product_id - matches by slug and updates
|
|
|
|
|
{:ok, product2, :updated} =
|
|
|
|
|
Products.upsert_product(conn, %{
|
|
|
|
|
provider_product_id: "new-product-id",
|
|
|
|
|
title: "Same Title Product",
|
|
|
|
|
provider_data: %{"id" => 2}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Should be the same product, with updated provider_product_id
|
|
|
|
|
assert product2.id == product1.id
|
|
|
|
|
assert product2.provider_product_id == "new-product-id"
|
|
|
|
|
assert product2.slug == "same-title-product"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "different titles create different slugs successfully" do
|
|
|
|
|
conn = provider_connection_fixture()
|
|
|
|
|
|
|
|
|
|
{:ok, product1, :created} =
|
|
|
|
|
Products.upsert_product(conn, %{
|
|
|
|
|
provider_product_id: "product-1",
|
|
|
|
|
title: "First Product",
|
|
|
|
|
provider_data: %{"id" => 1}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
{:ok, product2, :created} =
|
|
|
|
|
Products.upsert_product(conn, %{
|
|
|
|
|
provider_product_id: "product-2",
|
|
|
|
|
title: "Second Product",
|
|
|
|
|
provider_data: %{"id" => 2}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert product1.slug == "first-product"
|
|
|
|
|
assert product2.slug == "second-product"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|