Files
template-vitrine/backend/test/orders/service_test.go
T
Xor290 919c807004
ci-api / test (push) Failing after 8m7s
ci-web / test (push) Failing after 5m5s
chore: build
2026-09-20 12:18:33 +02:00

230 lines
6.1 KiB
Go

package orders_test
import (
"context"
"errors"
"testing"
"github.com/google/uuid"
"backend/internal/modules/orders"
"backend/internal/modules/pricing"
"backend/internal/modules/products"
"backend/internal/modules/units"
)
// fakeRepository is an in-memory orders.Repository used to unit-test
// orders.Service without a real database.
type fakeRepository struct {
orders map[uuid.UUID]*orders.Order
items map[uuid.UUID][]*orders.OrderItem
}
func newFakeRepository() *fakeRepository {
return &fakeRepository{orders: map[uuid.UUID]*orders.Order{}, items: map[uuid.UUID][]*orders.OrderItem{}}
}
func (r *fakeRepository) Create(_ context.Context, order *orders.Order, items []*orders.OrderItem) error {
cp := *order
r.orders[order.ID] = &cp
for _, item := range items {
item.OrderID = order.ID
}
r.items[order.ID] = items
return nil
}
func (r *fakeRepository) FindByID(_ context.Context, id uuid.UUID) (*orders.Order, []*orders.OrderItem, error) {
o, ok := r.orders[id]
if !ok {
return nil, nil, orders.ErrNotFound
}
return o, r.items[id], nil
}
func (r *fakeRepository) List(_ context.Context) ([]*orders.Order, error) {
var list []*orders.Order
for _, o := range r.orders {
list = append(list, o)
}
return list, nil
}
func (r *fakeRepository) ListByCustomer(_ context.Context, customerID uuid.UUID) ([]*orders.Order, error) {
var list []*orders.Order
for _, o := range r.orders {
if o.CustomerID != nil && *o.CustomerID == customerID {
list = append(list, o)
}
}
return list, nil
}
// fakeProducts / fakeUnits / fakePrices back the small consumer-defined
// interfaces orders.Service depends on.
type fakeProducts struct {
byID map[uuid.UUID]*products.Product
}
func (f *fakeProducts) Get(_ context.Context, id uuid.UUID) (*products.Product, error) {
p, ok := f.byID[id]
if !ok {
return nil, products.ErrNotFound
}
return p, nil
}
type fakeUnits struct {
byID map[uuid.UUID]*units.Unit
}
func (f *fakeUnits) Get(_ context.Context, id uuid.UUID) (*units.Unit, error) {
u, ok := f.byID[id]
if !ok {
return nil, units.ErrNotFound
}
return u, nil
}
type fakePrices struct {
byID map[uuid.UUID]*pricing.PriceTier
}
func (f *fakePrices) PriceForQuantity(_ context.Context, tierID uuid.UUID, multiplier int64) (int64, *pricing.PriceTier, error) {
t, ok := f.byID[tierID]
if !ok {
return 0, nil, pricing.ErrNotFound
}
if multiplier < 1 {
multiplier = 1
}
return t.PriceCents * multiplier, t, nil
}
type fakeNotifier struct {
newOrderCalls []string
}
func (f *fakeNotifier) NotifyNewOrder(_ context.Context, summary string) {
f.newOrderCalls = append(f.newOrderCalls, summary)
}
// testFixture wires a full set of fakes with one product/unit/tier, ready
// for a service under test.
type testFixture struct {
repo *fakeRepository
products *fakeProducts
units *fakeUnits
prices *fakePrices
notifier *fakeNotifier
productID uuid.UUID
unitID uuid.UUID
tierID uuid.UUID
}
func newFixture() *testFixture {
productID := uuid.New()
unitID := uuid.New()
tierID := uuid.New()
return &testFixture{
repo: newFakeRepository(),
products: &fakeProducts{byID: map[uuid.UUID]*products.Product{
productID: {ID: productID, Name: "Honey jar", IsActive: true},
}},
units: &fakeUnits{byID: map[uuid.UUID]*units.Unit{
unitID: {ID: unitID, Name: "Kilogram", Symbol: "kg"},
}},
prices: &fakePrices{byID: map[uuid.UUID]*pricing.PriceTier{
tierID: {ID: tierID, ProductID: productID, UnitID: unitID, Quantity: 5, PriceCents: 4500},
}},
notifier: &fakeNotifier{},
productID: productID,
unitID: unitID,
tierID: tierID,
}
}
func (f *testFixture) service() *orders.Service {
return orders.NewService(f.repo, f.products, f.units, f.prices, f.notifier)
}
func TestService_Create_ComputesTotalAndNotifies(t *testing.T) {
f := newFixture()
svc := f.service()
order, items, err := svc.Create(context.Background(), orders.CreateInput{
CustomerName: "Alice",
CustomerEmail: "alice@example.com",
Items: []orders.ItemInput{
{ProductID: f.productID, PriceTierID: f.tierID, Multiplier: 2},
},
})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if order.TotalCents != 9000 {
t.Fatalf("Create() order.TotalCents = %d, want 9000 (2x 4500)", order.TotalCents)
}
if len(items) != 1 || items[0].ProductName != "Honey jar" || items[0].UnitSymbol != "kg" {
t.Fatalf("Create() items = %+v, want snapshot of product name/unit symbol", items)
}
if len(f.notifier.newOrderCalls) != 1 {
t.Fatalf("Create() triggered %d new-order notifications, want exactly 1", len(f.notifier.newOrderCalls))
}
}
func TestService_Create_RejectsEmptyOrder(t *testing.T) {
f := newFixture()
svc := f.service()
_, _, err := svc.Create(context.Background(), orders.CreateInput{CustomerName: "Alice", CustomerEmail: "a@example.com"})
if !errors.Is(err, orders.ErrEmptyOrder) {
t.Fatalf("Create() error = %v, want ErrEmptyOrder", err)
}
}
func TestService_Create_RejectsTierProductMismatch(t *testing.T) {
f := newFixture()
svc := f.service()
otherProduct := uuid.New()
f.products.byID[otherProduct] = &products.Product{ID: otherProduct, Name: "Other", IsActive: true}
_, _, err := svc.Create(context.Background(), orders.CreateInput{
CustomerName: "Alice",
CustomerEmail: "alice@example.com",
Items: []orders.ItemInput{
// tierID actually belongs to f.productID, not otherProduct.
{ProductID: otherProduct, PriceTierID: f.tierID, Multiplier: 1},
},
})
if !errors.Is(err, orders.ErrProductMismatch) {
t.Fatalf("Create() error = %v, want ErrProductMismatch", err)
}
}
func TestService_List_ReturnsAllOrders(t *testing.T) {
f := newFixture()
svc := f.service()
for range 2 {
if _, _, err := svc.Create(context.Background(), orders.CreateInput{
CustomerName: "Alice",
CustomerEmail: "alice@example.com",
Items: []orders.ItemInput{{ProductID: f.productID, PriceTierID: f.tierID, Multiplier: 1}},
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
}
list, err := svc.List(context.Background())
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(list) != 2 {
t.Fatalf("List() returned %d orders, want 2", len(list))
}
}