feat: Add currency management feature with CRUD operations
Some checks failed
Run Tests / test (push) Failing after 5m2s
Some checks failed
Run Tests / test (push) Failing after 5m2s
- Introduced a new template for currency overview and management (`currencies.html`). - Updated footer to include attribution to AllYouCanGET. - Added "Currencies" link to the main navigation header. - Implemented end-to-end tests for currency creation, update, and activation toggling. - Created unit tests for currency API endpoints, including creation, updating, and activation toggling. - Added a fixture to seed default currencies for testing. - Enhanced database setup tests to ensure proper seeding and migration handling.
This commit is contained in:
@@ -64,6 +64,40 @@ def live_server() -> Generator[str, None, None]:
|
||||
process.wait(timeout=5)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def seed_default_currencies(live_server: str) -> None:
|
||||
"""Ensure a baseline set of currencies exists for UI flows."""
|
||||
|
||||
seeds = [
|
||||
{"code": "EUR", "name": "Euro", "symbol": "EUR", "is_active": True},
|
||||
{"code": "CLP", "name": "Chilean Peso", "symbol": "CLP$", "is_active": True},
|
||||
]
|
||||
|
||||
with httpx.Client(base_url=live_server, timeout=5.0) as client:
|
||||
try:
|
||||
response = client.get("/api/currencies/?include_inactive=true")
|
||||
response.raise_for_status()
|
||||
existing_codes = {
|
||||
str(item.get("code"))
|
||||
for item in response.json()
|
||||
if isinstance(item, dict) and item.get("code")
|
||||
}
|
||||
except httpx.HTTPError as exc: # noqa: BLE001
|
||||
raise RuntimeError("Failed to read existing currencies") from exc
|
||||
|
||||
for payload in seeds:
|
||||
if payload["code"] in existing_codes:
|
||||
continue
|
||||
try:
|
||||
create_response = client.post("/api/currencies/", json=payload)
|
||||
except httpx.HTTPError as exc: # noqa: BLE001
|
||||
raise RuntimeError("Failed to seed currencies") from exc
|
||||
|
||||
if create_response.status_code == 409:
|
||||
continue
|
||||
create_response.raise_for_status()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def playwright_instance() -> Generator[Playwright, None, None]:
|
||||
"""Provide a Playwright instance for the test session."""
|
||||
|
||||
130
tests/e2e/test_currencies.py
Normal file
130
tests/e2e/test_currencies.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
def _unique_currency_code(existing: set[str]) -> str:
|
||||
"""Generate a unique three-letter code not present in *existing*."""
|
||||
alphabet = string.ascii_uppercase
|
||||
for _ in range(100):
|
||||
candidate = "".join(random.choices(alphabet, k=3))
|
||||
if candidate not in existing and candidate != "USD":
|
||||
return candidate
|
||||
raise AssertionError(
|
||||
"Unable to generate a unique currency code for the test run.")
|
||||
|
||||
|
||||
def _metric_value(page: Page, element_id: str) -> int:
|
||||
locator = page.locator(f"#{element_id}")
|
||||
expect(locator).to_be_visible()
|
||||
return int(locator.inner_text().strip())
|
||||
|
||||
|
||||
def _expect_feedback(page: Page, expected_text: str) -> None:
|
||||
page.wait_for_function(
|
||||
"expected => {"
|
||||
" const el = document.getElementById('currency-form-feedback');"
|
||||
" if (!el) return false;"
|
||||
" const text = (el.textContent || '').trim();"
|
||||
" return !el.classList.contains('hidden') && text === expected;"
|
||||
"}",
|
||||
arg=expected_text,
|
||||
)
|
||||
feedback = page.locator("#currency-form-feedback")
|
||||
expect(feedback).to_have_text(expected_text)
|
||||
|
||||
|
||||
def test_currency_workflow_create_update_toggle(page: Page) -> None:
|
||||
"""Exercise create, update, and toggle flows on the currency settings page."""
|
||||
page.goto("/ui/currencies")
|
||||
expect(page).to_have_title("Currencies · CalMiner")
|
||||
expect(page.locator("h2:has-text('Currency Overview')")).to_be_visible()
|
||||
|
||||
code_cells = page.locator("#currencies-table-body tr td:nth-child(1)")
|
||||
existing_codes = {text.strip().upper()
|
||||
for text in code_cells.all_inner_texts()}
|
||||
|
||||
total_before = _metric_value(page, "currency-metric-total")
|
||||
active_before = _metric_value(page, "currency-metric-active")
|
||||
inactive_before = _metric_value(page, "currency-metric-inactive")
|
||||
|
||||
new_code = _unique_currency_code(existing_codes)
|
||||
new_name = f"Test Currency {new_code}"
|
||||
new_symbol = new_code[0]
|
||||
|
||||
page.fill("#currency-form-code", new_code)
|
||||
page.fill("#currency-form-name", new_name)
|
||||
page.fill("#currency-form-symbol", new_symbol)
|
||||
page.select_option("#currency-form-status", "true")
|
||||
|
||||
with page.expect_response("**/api/currencies/") as create_info:
|
||||
page.click("button[type='submit']")
|
||||
create_response = create_info.value
|
||||
assert create_response.status == 201
|
||||
|
||||
_expect_feedback(page, "Currency created successfully.")
|
||||
|
||||
page.wait_for_function(
|
||||
"expected => Number(document.getElementById('currency-metric-total').textContent.trim()) === expected",
|
||||
arg=total_before + 1,
|
||||
)
|
||||
page.wait_for_function(
|
||||
"expected => Number(document.getElementById('currency-metric-active').textContent.trim()) === expected",
|
||||
arg=active_before + 1,
|
||||
)
|
||||
|
||||
row = page.locator("#currencies-table-body tr").filter(has_text=new_code)
|
||||
expect(row).to_be_visible()
|
||||
expect(row.locator("td").nth(3)).to_have_text("Active")
|
||||
|
||||
# Switch to update mode using the existing currency option.
|
||||
page.select_option("#currency-form-existing", new_code)
|
||||
updated_name = f"{new_name} Updated"
|
||||
updated_symbol = f"{new_symbol}$"
|
||||
page.fill("#currency-form-name", updated_name)
|
||||
page.fill("#currency-form-symbol", updated_symbol)
|
||||
page.select_option("#currency-form-status", "false")
|
||||
|
||||
with page.expect_response(f"**/api/currencies/{new_code}") as update_info:
|
||||
page.click("button[type='submit']")
|
||||
update_response = update_info.value
|
||||
assert update_response.status == 200
|
||||
|
||||
_expect_feedback(page, "Currency updated successfully.")
|
||||
|
||||
page.wait_for_function(
|
||||
"expected => Number(document.getElementById('currency-metric-active').textContent.trim()) === expected",
|
||||
arg=active_before,
|
||||
)
|
||||
page.wait_for_function(
|
||||
"expected => Number(document.getElementById('currency-metric-inactive').textContent.trim()) === expected",
|
||||
arg=inactive_before + 1,
|
||||
)
|
||||
|
||||
expect(row.locator("td").nth(1)).to_have_text(updated_name)
|
||||
expect(row.locator("td").nth(2)).to_have_text(updated_symbol)
|
||||
expect(row.locator("td").nth(3)).to_contain_text("Inactive")
|
||||
|
||||
toggle_button = row.locator("button[data-action='toggle']")
|
||||
expect(toggle_button).to_have_text("Activate")
|
||||
|
||||
with page.expect_response(f"**/api/currencies/{new_code}/activation") as toggle_info:
|
||||
toggle_button.click()
|
||||
toggle_response = toggle_info.value
|
||||
assert toggle_response.status == 200
|
||||
|
||||
page.wait_for_function(
|
||||
"expected => Number(document.getElementById('currency-metric-active').textContent.trim()) === expected",
|
||||
arg=active_before + 1,
|
||||
)
|
||||
page.wait_for_function(
|
||||
"expected => Number(document.getElementById('currency-metric-inactive').textContent.trim()) === expected",
|
||||
arg=inactive_before,
|
||||
)
|
||||
|
||||
_expect_feedback(page, f"Currency {new_code} activated.")
|
||||
|
||||
expect(row.locator("td").nth(3)).to_contain_text("Active")
|
||||
expect(row.locator("button[data-action='toggle']")
|
||||
).to_have_text("Deactivate")
|
||||
@@ -14,6 +14,7 @@ UI_ROUTES = [
|
||||
("/ui/maintenance", "Maintenance · CalMiner", "Maintenance Schedule"),
|
||||
("/ui/simulations", "Simulations · CalMiner", "Monte Carlo Simulations"),
|
||||
("/ui/reporting", "Reporting · CalMiner", "Scenario KPI Summary"),
|
||||
("/ui/currencies", "Currencies · CalMiner", "Currency Overview"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user