initial_commit

This commit is contained in:
MelvynFaith
2026-11-02 14:58:43 +07:00
commit b06be6e0a7
20 changed files with 547 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
"""Pengujian jalur independen validate_checkout (V(G) = 7).
Satu test per jalur basis. Nomor P1..P7 sama dengan tabel di docs/laporan.md.
Kebutuhan: FR-ORD-02, FR-ORD-03, FR-ORD-08.
"""
import pytest
from umkm.catalog import ClothingProduct
from umkm.checkout import (
CartItem,
CheckoutSummary,
InsufficientStockError,
InvalidQuantityError,
MultiShopError,
ProductNotFoundError,
validate_checkout,
)
@pytest.fixture
def products():
return {
"p1": ClothingProduct("p1", "shop-A", "Kaos", price=50_000, on_hand=10, size="M"),
"p2": ClothingProduct("p2", "shop-A", "Kemeja", price=120_000, on_hand=5, size="L"),
"p3": ClothingProduct("p3", "shop-B", "Jaket", price=200_000, on_hand=5, size="XL"),
"p4": ClothingProduct("p4", "shop-A", "Topi", price=30_000, on_hand=3, size="S", reserved=3),
}
def test_p1_keranjang_kosong_total_nol(products):
"""P1: loop tidak dimasuki."""
assert validate_checkout([], products) == CheckoutSummary(None, 0)
def test_p2_produk_tidak_ada(products):
"""P2: product is None -> ProductNotFoundError."""
with pytest.raises(ProductNotFoundError):
validate_checkout([CartItem("tidak-ada", 1)], products)
@pytest.mark.parametrize("qty", [0, -1])
def test_p3_kuantitas_tidak_valid(products, qty):
"""P3: quantity <= 0 -> InvalidQuantityError (batas 0 dan negatif)."""
with pytest.raises(InvalidQuantityError):
validate_checkout([CartItem("p1", qty)], products)
def test_p4_stok_tidak_cukup_pada_item_pertama(products):
"""P4: available < quantity pada iterasi pertama."""
with pytest.raises(InsufficientStockError):
validate_checkout([CartItem("p4", 1)], products)
def test_p5_satu_item_valid(products):
"""P5: jalur baseline, satu item lolos semua cek."""
assert validate_checkout([CartItem("p1", 2)], products) == CheckoutSummary("shop-A", 100_000)
def test_p6_dua_item_satu_toko(products):
"""P6: iterasi kedua, toko sama -> shop_id tidak berubah, total dijumlahkan."""
result = validate_checkout([CartItem("p1", 2), CartItem("p2", 1)], products)
assert result == CheckoutSummary("shop-A", 2 * 50_000 + 120_000)
def test_p7_dua_item_beda_toko(products):
"""P7: iterasi kedua, toko berbeda -> MultiShopError (FR-ORD-08)."""
with pytest.raises(MultiShopError):
validate_checkout([CartItem("p1", 1), CartItem("p3", 1)], products)
def test_batas_stok_tepat_sama_lolos(products):
"""Batas available == quantity harus lolos (cabang stok false)."""
assert validate_checkout([CartItem("p2", 5)], products).total_amount == 600_000
+123
View File
@@ -0,0 +1,123 @@
"""Pengujian mesin status Order: transisi valid, tidak valid, dan guard pelaku.
Kebutuhan: FR-ORD-04, FR-ORD-06, FR-ORD-09, FR-ORD-10.
"""
from datetime import datetime, timedelta
import pytest
from umkm.order import (
TRANSITIONS,
InvalidTransitionError,
Order,
OrderStatus as S,
PermissionDeniedError,
)
BUYER, SELLER = "buyer-1", "seller-1"
T0 = datetime(2026, 11, 1, 10, 0)
def new_order():
return Order("o-1", BUYER, SELLER)
def order_in(status):
"""Bawa order ke status yang diminta lewat transisi sungguhan."""
order = new_order()
if status in (S.PAID, S.SHIPPED, S.COMPLETED):
order.pay()
if status in (S.SHIPPED, S.COMPLETED):
order.ship(SELLER, T0)
if status is S.COMPLETED:
order.confirm_received(BUYER)
if status is S.CANCELLED:
order.cancel(BUYER)
if status is S.EXPIRED:
order.expire()
assert order.status is status
return order
# Semua event yang dapat dipicu; auto_complete memakai waktu tepat jatuh tempo.
EVENTS = {
"pay": lambda o: o.pay(),
"cancel": lambda o: o.cancel(BUYER),
"expire": lambda o: o.expire(),
"ship": lambda o: o.ship(SELLER, T0),
"complete": lambda o: o.confirm_received(BUYER),
"auto_complete": lambda o: o.auto_complete(T0 + timedelta(days=3)),
}
VALID = dict(TRANSITIONS)
VALID[(S.SHIPPED, "auto_complete")] = S.COMPLETED
INVALID = [(s, e) for s in S for e in EVENTS if (s, e) not in VALID]
def test_order_baru_berstatus_pending_payment():
assert new_order().status is S.PENDING_PAYMENT
@pytest.mark.parametrize(("src", "event"), list(VALID))
def test_transisi_valid(src, event):
order = order_in(src)
EVENTS[event](order)
assert order.status is VALID[(src, event)]
@pytest.mark.parametrize(("src", "event"), INVALID)
def test_transisi_tidak_valid_ditolak_dan_status_tetap(src, event):
order = order_in(src)
with pytest.raises(InvalidTransitionError):
EVENTS[event](order)
assert order.status is src
def test_jumlah_kombinasi_lengkap():
"""6 status x 6 event = 36 kombinasi: 6 valid + 30 tidak valid."""
assert len(VALID) == 6
assert len(INVALID) == 30
@pytest.mark.parametrize("final", [S.COMPLETED, S.CANCELLED, S.EXPIRED])
def test_status_akhir_tidak_punya_transisi_keluar(final):
assert not [key for key in VALID if key[0] is final]
def test_seller_tidak_boleh_menyelesaikan_order():
"""FR-ORD-10: hanya pembeli yang mengonfirmasi."""
order = order_in(S.SHIPPED)
with pytest.raises(PermissionDeniedError):
order.confirm_received(SELLER)
assert order.status is S.SHIPPED
def test_hanya_seller_pemilik_toko_boleh_mengirim():
order = order_in(S.PAID)
with pytest.raises(PermissionDeniedError):
order.ship("seller-lain", T0)
assert order.status is S.PAID
def test_hanya_pembeli_pemilik_order_boleh_membatalkan():
order = new_order()
with pytest.raises(PermissionDeniedError):
order.cancel("buyer-lain")
assert order.status is S.PENDING_PAYMENT
def test_auto_complete_ditolak_sebelum_3_hari():
order = order_in(S.SHIPPED)
with pytest.raises(InvalidTransitionError):
order.auto_complete(T0 + timedelta(days=3) - timedelta(seconds=1))
assert order.status is S.SHIPPED
def test_auto_complete_tepat_3_hari_berhasil():
order = order_in(S.SHIPPED)
order.auto_complete(T0 + timedelta(days=3))
assert order.status is S.COMPLETED
def test_ship_mencatat_waktu_pengiriman():
assert order_in(S.SHIPPED).shipped_at == T0
+68
View File
@@ -0,0 +1,68 @@
"""Pewarisan dan polimorfisme hierarki Product (AR-01, FR-CAT-03)."""
from datetime import date
import pytest
from umkm.catalog import ClothingProduct, FoodProduct, Product
TODAY = date(2026, 11, 2)
def make_products():
return [
FoodProduct("f1", "shop-A", "Keripik", 15_000, 10, expiry_date=date(2026, 12, 1)),
ClothingProduct("c1", "shop-A", "Kaos", 50_000, 10, size="M"),
]
def test_subclass_adalah_product():
assert all(isinstance(p, Product) for p in make_products())
def test_product_abstrak_tidak_bisa_dibuat():
with pytest.raises(TypeError):
Product("x", "s", "x", 1, 1)
def test_attributes_polimorfik_lewat_referensi_basis():
"""Pemanggilan yang sama pada tipe basis memberi hasil berbeda per subclass."""
hasil = {p.category: p.attributes() for p in make_products()}
assert hasil == {"food": {"expiryDate": "2026-12-01"}, "clothing": {"size": "M"}}
def test_available_diwarisi_semua_subclass():
for p in make_products():
p.reserved = 4
assert p.available == 6
def test_food_kedaluwarsa_tidak_bisa_dipesan_override():
food = FoodProduct("f1", "s", "Keripik", 1, 10, expiry_date=date(2026, 11, 1))
assert food.is_orderable(TODAY) is False
assert food.is_orderable(date(2026, 11, 1)) is True # hari kedaluwarsa masih boleh
def test_food_stok_habis_tidak_bisa_dipesan_lewat_super():
food = FoodProduct("f1", "s", "Keripik", 1, 5, expiry_date=date(2027, 1, 1), reserved=5)
assert food.is_orderable(TODAY) is False
def test_clothing_memakai_aturan_dasar_tanpa_override():
assert "is_orderable" not in ClothingProduct.__dict__
assert ClothingProduct("c", "s", "Kaos", 1, 1, size="S").is_orderable(TODAY) is True
assert ClothingProduct("c", "s", "Kaos", 1, 1, size="S", reserved=1).is_orderable(TODAY) is False
def test_clothing_size_tidak_valid_ditolak():
with pytest.raises(ValueError):
ClothingProduct("c", "s", "Kaos", 1, 1, size="XXL")
@pytest.mark.parametrize(
("price", "on_hand", "reserved"),
[(-1, 1, 0), (1, 1, 2), (1, 1, -1)],
)
def test_validasi_konstruktor_basis(price, on_hand, reserved):
with pytest.raises(ValueError):
ClothingProduct("c", "s", "Kaos", price, on_hand, size="M", reserved=reserved)