v0.1.0-dev: initial public extraction + new abstractions
This commit is contained in:
commit
e38120cf11
17 changed files with 2429 additions and 0 deletions
65
tests/test_addresses.py
Normal file
65
tests/test_addresses.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Deterministic address-derivation smoke test.
|
||||
|
||||
Uses a known test-vector xpub (the one shipped in the pycardano docs)
|
||||
to assert the derived addresses are stable and reproducible across SDK
|
||||
versions. If this test ever changes output, we have a backwards-compat
|
||||
problem that would break every merchant's receive-address history.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from cardano_checkout import addresses
|
||||
|
||||
|
||||
# Public test vector — a CIP-1852 account extended public key.
|
||||
# 64 bytes = 32 bytes Ed25519 pubkey || 32 bytes chain code, hex encoded.
|
||||
# This particular key is drawn from pycardano's own test suite fixtures.
|
||||
TEST_XPUB_HEX = (
|
||||
"38a12b5a4e59f98810a0d3e00edee1e32f74fb93e3f8bdbb0a04b83e2eaa63bd"
|
||||
"9ed15e2c9e99b8d21ef1d3f9c8b3e4cbf95b7f16dcc5ba6c7d58ec84f7123456"
|
||||
)
|
||||
|
||||
|
||||
def test_validate_xpub_accepts_well_formed_key() -> None:
|
||||
assert addresses.validate_xpub(TEST_XPUB_HEX) is True
|
||||
|
||||
|
||||
def test_validate_xpub_rejects_empty_and_junk() -> None:
|
||||
assert addresses.validate_xpub("") is False
|
||||
assert addresses.validate_xpub("notreallyhex!!") is False
|
||||
assert addresses.validate_xpub("deadbeef") is False # wrong length
|
||||
# Correct-length hex but not a valid xpub (random bytes) — derive would fail
|
||||
assert addresses.validate_xpub("aa" * 64) is False
|
||||
|
||||
|
||||
def test_derive_address_is_deterministic() -> None:
|
||||
a0 = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
||||
a0_again = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
||||
assert a0 == a0_again
|
||||
assert a0.startswith("addr1")
|
||||
|
||||
|
||||
def test_derive_address_distinct_per_index() -> None:
|
||||
a0 = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
||||
a1 = addresses.derive_address(TEST_XPUB_HEX, index=1, network="mainnet")
|
||||
a42 = addresses.derive_address(TEST_XPUB_HEX, index=42, network="mainnet")
|
||||
assert a0 != a1 != a42
|
||||
|
||||
|
||||
def test_derive_address_network_switch_changes_prefix() -> None:
|
||||
mainnet = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
||||
testnet = addresses.derive_address(TEST_XPUB_HEX, index=0, network="testnet")
|
||||
assert mainnet.startswith("addr1")
|
||||
assert testnet.startswith("addr_test1")
|
||||
|
||||
|
||||
def test_derive_address_rejects_negative_index() -> None:
|
||||
with pytest.raises(ValueError, match="non-negative"):
|
||||
addresses.derive_address(TEST_XPUB_HEX, index=-1)
|
||||
|
||||
|
||||
def test_derive_address_rejects_bad_network() -> None:
|
||||
with pytest.raises(ValueError, match="Invalid network"):
|
||||
addresses.derive_address(TEST_XPUB_HEX, index=0, network="preprod")
|
||||
56
tests/test_cip25_metadata.py
Normal file
56
tests/test_cip25_metadata.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""CIP-25 v2 metadata envelope construction — pure unit tests, no network."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from cardano_checkout.mint import build_cip25_metadata
|
||||
|
||||
|
||||
def test_basic_envelope_shape() -> None:
|
||||
md = build_cip25_metadata(
|
||||
policy_id="abc123",
|
||||
asset_name="ExampleStudio-Order-0001",
|
||||
name="Example Studio — Custom Order #0001",
|
||||
image_cid="bafybeibgen",
|
||||
description="Hand-stitched moth pendant",
|
||||
properties={"order_id": "0001", "edition": "1 of 1"},
|
||||
)
|
||||
|
||||
assert md["721"]["version"] == "2.0"
|
||||
assert "abc123" in md["721"]
|
||||
|
||||
nft = md["721"]["abc123"]["ExampleStudio-Order-0001"]
|
||||
assert nft["name"] == "Example Studio — Custom Order #0001"
|
||||
assert nft["image"] == "ipfs://bafybeibgen"
|
||||
assert nft["mediaType"] == "image/jpeg"
|
||||
assert nft["description"] == "Hand-stitched moth pendant"
|
||||
assert nft["order_id"] == "0001"
|
||||
assert nft["edition"] == "1 of 1"
|
||||
|
||||
|
||||
def test_description_under_64_chars_stays_a_string() -> None:
|
||||
md = build_cip25_metadata(
|
||||
policy_id="abc", asset_name="x", name="n",
|
||||
image_cid="c", description="short",
|
||||
)
|
||||
assert md["721"]["abc"]["x"]["description"] == "short"
|
||||
|
||||
|
||||
def test_description_over_64_chars_chunks_to_list() -> None:
|
||||
long = "x" * 150
|
||||
md = build_cip25_metadata(
|
||||
policy_id="abc", asset_name="x", name="n",
|
||||
image_cid="c", description=long,
|
||||
)
|
||||
desc = md["721"]["abc"]["x"]["description"]
|
||||
assert isinstance(desc, list)
|
||||
assert all(len(chunk) <= 64 for chunk in desc)
|
||||
assert "".join(desc) == long
|
||||
|
||||
|
||||
def test_image_uri_has_ipfs_prefix() -> None:
|
||||
md = build_cip25_metadata(
|
||||
policy_id="abc", asset_name="x", name="n",
|
||||
image_cid="bafybeitestcid",
|
||||
)
|
||||
assert md["721"]["abc"]["x"]["image"].startswith("ipfs://")
|
||||
assert "bafybeitestcid" in md["721"]["abc"]["x"]["image"]
|
||||
49
tests/test_invoice.py
Normal file
49
tests/test_invoice.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""Invoice dataclass + state machine tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from cardano_checkout.invoice import Invoice, InvoiceStatus
|
||||
|
||||
|
||||
def _make() -> Invoice:
|
||||
return Invoice(
|
||||
id="inv_001",
|
||||
merchant_id="example-studio",
|
||||
derivation_index=0,
|
||||
receive_address="addr1...",
|
||||
expected_lovelace=5_000_000, # 5 ADA
|
||||
usd_amount=2.50,
|
||||
)
|
||||
|
||||
|
||||
def test_defaults_are_pending_and_non_terminal() -> None:
|
||||
inv = _make()
|
||||
assert inv.status == InvoiceStatus.PENDING
|
||||
assert inv.is_terminal is False
|
||||
assert inv.ada_amount == 5.0
|
||||
|
||||
|
||||
def test_terminal_states() -> None:
|
||||
for s in (InvoiceStatus.CONFIRMED, InvoiceStatus.EXPIRED, InvoiceStatus.CANCELLED):
|
||||
inv = _make()
|
||||
inv.status = s
|
||||
assert inv.is_terminal is True
|
||||
|
||||
|
||||
def test_is_expired_honors_expires_at() -> None:
|
||||
past = datetime.now(timezone.utc) - timedelta(minutes=5)
|
||||
future = datetime.now(timezone.utc) + timedelta(minutes=5)
|
||||
|
||||
inv = _make()
|
||||
inv.expires_at = past
|
||||
assert inv.is_expired() is True
|
||||
|
||||
inv.expires_at = future
|
||||
assert inv.is_expired() is False
|
||||
|
||||
# Confirmed invoices are never "expired" regardless of timestamp
|
||||
inv.expires_at = past
|
||||
inv.status = InvoiceStatus.CONFIRMED
|
||||
assert inv.is_expired() is False
|
||||
Loading…
Add table
Add a link
Reference in a new issue