add reviews schema and context (phase 2)
All checks were successful
deploy / deploy (push) Successful in 1m4s
All checks were successful
deploy / deploy (push) Successful in 1m4s
- Create reviews table with product/order associations - Add Review schema with create/update/moderation changesets - Add Reviews context with CRUD, purchase verification, aggregates - Add get_images/1 to Media, get_variant/1 to Products - 23 tests covering all context functions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
385
test/berrypod/reviews_test.exs
Normal file
385
test/berrypod/reviews_test.exs
Normal file
@@ -0,0 +1,385 @@
|
||||
defmodule Berrypod.ReviewsTest do
|
||||
use Berrypod.DataCase, async: true
|
||||
|
||||
alias Berrypod.Reviews
|
||||
alias Berrypod.Reviews.Review
|
||||
|
||||
import Berrypod.ProductsFixtures
|
||||
import Berrypod.OrdersFixtures
|
||||
|
||||
# ── Create ─────────────────────────────────────────────────────────
|
||||
|
||||
describe "create_review/1" do
|
||||
test "creates a review with valid attrs" do
|
||||
product = product_fixture()
|
||||
|
||||
attrs = %{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
}
|
||||
|
||||
assert {:ok, %Review{} = review} = Reviews.create_review(attrs)
|
||||
assert review.product_id == product.id
|
||||
assert review.email == "buyer@example.com"
|
||||
assert review.author_name == "Jane"
|
||||
assert review.rating == 5
|
||||
assert review.status == "pending"
|
||||
end
|
||||
|
||||
test "normalises email to lowercase" do
|
||||
product = product_fixture()
|
||||
|
||||
attrs = %{
|
||||
product_id: product.id,
|
||||
email: "BUYER@EXAMPLE.COM",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
}
|
||||
|
||||
{:ok, review} = Reviews.create_review(attrs)
|
||||
assert review.email == "buyer@example.com"
|
||||
end
|
||||
|
||||
test "auto-links to matching order for verified purchase badge" do
|
||||
conn = provider_connection_fixture(%{provider_type: "printify"})
|
||||
product = product_fixture(%{provider_connection: conn})
|
||||
variant = product_variant_fixture(%{product: product})
|
||||
|
||||
order =
|
||||
order_fixture(%{
|
||||
customer_email: "buyer@example.com",
|
||||
payment_status: "paid",
|
||||
variant_id: variant.id
|
||||
})
|
||||
|
||||
attrs = %{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
}
|
||||
|
||||
{:ok, review} = Reviews.create_review(attrs)
|
||||
assert review.order_id == order.id
|
||||
end
|
||||
|
||||
test "allows optional title and body" do
|
||||
product = product_fixture()
|
||||
|
||||
attrs = %{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 4,
|
||||
title: "Great product!",
|
||||
body: "Would buy again."
|
||||
}
|
||||
|
||||
{:ok, review} = Reviews.create_review(attrs)
|
||||
assert review.title == "Great product!"
|
||||
assert review.body == "Would buy again."
|
||||
end
|
||||
|
||||
test "validates rating is 1-5" do
|
||||
product = product_fixture()
|
||||
|
||||
attrs = %{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 6
|
||||
}
|
||||
|
||||
assert {:error, changeset} = Reviews.create_review(attrs)
|
||||
assert "is invalid" in errors_on(changeset).rating
|
||||
end
|
||||
|
||||
test "enforces one review per email+product" do
|
||||
product = product_fixture()
|
||||
|
||||
attrs = %{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
}
|
||||
|
||||
{:ok, _} = Reviews.create_review(attrs)
|
||||
{:error, changeset} = Reviews.create_review(attrs)
|
||||
assert "you have already reviewed this product" in errors_on(changeset).email
|
||||
end
|
||||
end
|
||||
|
||||
# ── Read ───────────────────────────────────────────────────────────
|
||||
|
||||
describe "get_review/1" do
|
||||
test "returns the review" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, review} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
assert Reviews.get_review(review.id) == review
|
||||
end
|
||||
|
||||
test "returns nil for unknown id" do
|
||||
assert Reviews.get_review(Ecto.UUID.generate()) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_review_by_email_and_product/2" do
|
||||
test "returns the review for the email+product" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, review} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
assert Reviews.get_review_by_email_and_product("buyer@example.com", product.id) == review
|
||||
end
|
||||
|
||||
test "returns nil when no review exists" do
|
||||
product = product_fixture()
|
||||
assert Reviews.get_review_by_email_and_product("nobody@example.com", product.id) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_reviews_for_product/2" do
|
||||
test "lists only approved reviews by default" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, pending} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "pending@example.com",
|
||||
author_name: "Pending",
|
||||
rating: 3
|
||||
})
|
||||
|
||||
{:ok, approved} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "approved@example.com",
|
||||
author_name: "Approved",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
Reviews.approve_review(approved)
|
||||
|
||||
reviews = Reviews.list_reviews_for_product(product.id)
|
||||
assert length(reviews) == 1
|
||||
assert hd(reviews).email == "approved@example.com"
|
||||
refute Enum.any?(reviews, &(&1.id == pending.id))
|
||||
end
|
||||
|
||||
test "can filter by status" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, _} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "pending@example.com",
|
||||
author_name: "Pending",
|
||||
rating: 3
|
||||
})
|
||||
|
||||
reviews = Reviews.list_reviews_for_product(product.id, status: "pending")
|
||||
assert length(reviews) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_pending_reviews/0" do
|
||||
test "lists pending reviews with preloaded product" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, _} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
reviews = Reviews.list_pending_reviews()
|
||||
assert length(reviews) == 1
|
||||
assert hd(reviews).product.id == product.id
|
||||
end
|
||||
end
|
||||
|
||||
# ── Update ─────────────────────────────────────────────────────────
|
||||
|
||||
describe "update_review/2" do
|
||||
test "updates the review and resets status to pending" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, review} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
Reviews.approve_review(review)
|
||||
|
||||
{:ok, updated} = Reviews.update_review(review, %{rating: 4, body: "Updated!"})
|
||||
assert updated.rating == 4
|
||||
assert updated.body == "Updated!"
|
||||
assert updated.status == "pending"
|
||||
end
|
||||
end
|
||||
|
||||
# ── Moderation ─────────────────────────────────────────────────────
|
||||
|
||||
describe "approve_review/1" do
|
||||
test "sets status to approved" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, review} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
{:ok, approved} = Reviews.approve_review(review)
|
||||
assert approved.status == "approved"
|
||||
end
|
||||
end
|
||||
|
||||
describe "reject_review/1" do
|
||||
test "sets status to rejected" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, review} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
{:ok, rejected} = Reviews.reject_review(review)
|
||||
assert rejected.status == "rejected"
|
||||
end
|
||||
end
|
||||
|
||||
# ── Aggregates ─────────────────────────────────────────────────────
|
||||
|
||||
describe "average_rating_for_product/1" do
|
||||
test "returns average and count for approved reviews" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, r1} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "a@example.com",
|
||||
author_name: "A",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
{:ok, r2} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "b@example.com",
|
||||
author_name: "B",
|
||||
rating: 4
|
||||
})
|
||||
|
||||
Reviews.approve_review(r1)
|
||||
Reviews.approve_review(r2)
|
||||
|
||||
{avg, count} = Reviews.average_rating_for_product(product.id)
|
||||
assert Decimal.equal?(avg, Decimal.new("4.5"))
|
||||
assert count == 2
|
||||
end
|
||||
|
||||
test "returns {nil, 0} when no approved reviews" do
|
||||
product = product_fixture()
|
||||
assert {nil, 0} = Reviews.average_rating_for_product(product.id)
|
||||
end
|
||||
|
||||
test "excludes pending and rejected reviews" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, _} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "pending@example.com",
|
||||
author_name: "Pending",
|
||||
rating: 1
|
||||
})
|
||||
|
||||
assert {nil, 0} = Reviews.average_rating_for_product(product.id)
|
||||
end
|
||||
end
|
||||
|
||||
# ── Purchase verification ──────────────────────────────────────────
|
||||
|
||||
describe "can_review?/2" do
|
||||
test "returns true when email has purchased the product" do
|
||||
conn = provider_connection_fixture(%{provider_type: "printify"})
|
||||
product = product_fixture(%{provider_connection: conn})
|
||||
variant = product_variant_fixture(%{product: product})
|
||||
|
||||
order_fixture(%{
|
||||
customer_email: "buyer@example.com",
|
||||
payment_status: "paid",
|
||||
variant_id: variant.id
|
||||
})
|
||||
|
||||
assert Reviews.can_review?("buyer@example.com", product.id)
|
||||
end
|
||||
|
||||
test "returns false when email has not purchased the product" do
|
||||
product = product_fixture()
|
||||
refute Reviews.can_review?("nobody@example.com", product.id)
|
||||
end
|
||||
|
||||
test "returns false when order exists but for different product" do
|
||||
conn = provider_connection_fixture(%{provider_type: "printify"})
|
||||
product1 = product_fixture(%{provider_connection: conn})
|
||||
product2 = product_fixture(%{provider_connection: conn})
|
||||
variant1 = product_variant_fixture(%{product: product1})
|
||||
|
||||
order_fixture(%{
|
||||
customer_email: "buyer@example.com",
|
||||
payment_status: "paid",
|
||||
variant_id: variant1.id
|
||||
})
|
||||
|
||||
refute Reviews.can_review?("buyer@example.com", product2.id)
|
||||
end
|
||||
end
|
||||
|
||||
# ── Delete ─────────────────────────────────────────────────────────
|
||||
|
||||
describe "delete_review/1" do
|
||||
test "deletes the review" do
|
||||
product = product_fixture()
|
||||
|
||||
{:ok, review} =
|
||||
Reviews.create_review(%{
|
||||
product_id: product.id,
|
||||
email: "buyer@example.com",
|
||||
author_name: "Jane",
|
||||
rating: 5
|
||||
})
|
||||
|
||||
{:ok, _} = Reviews.delete_review(review)
|
||||
assert Reviews.get_review(review.id) == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user