python-getpaid-simulator 3.0.0a3__tar.gz → 3.0.0a4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. python_getpaid_simulator-3.0.0a4/.github/workflows/ci.yml +55 -0
  2. python_getpaid_simulator-3.0.0a4/Dockerfile.dockerignore +22 -0
  3. python_getpaid_simulator-3.0.0a4/Dockerfile.test +39 -0
  4. python_getpaid_simulator-3.0.0a4/Dockerfile.test.dockerignore +22 -0
  5. python_getpaid_simulator-3.0.0a4/Makefile +50 -0
  6. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/PKG-INFO +2 -2
  7. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/README.md +1 -1
  8. python_getpaid_simulator-3.0.0a4/compose.test.yml +30 -0
  9. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/docker-compose.yml +1 -3
  10. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/pyproject.toml +4 -3
  11. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/__init__.py +1 -1
  12. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/app.py +6 -4
  13. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/routes.py +45 -9
  14. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/e2e/conftest.py +26 -10
  15. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_app.py +58 -0
  16. python_getpaid_simulator-3.0.0a4/tests/test_smoke.py +43 -0
  17. python_getpaid_simulator-3.0.0a4/tests/test_test_infrastructure.py +102 -0
  18. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_ui_authorize.py +51 -0
  19. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_ui_dashboard.py +39 -0
  20. python_getpaid_simulator-3.0.0a3/tests/test_smoke.py +0 -22
  21. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/.gitignore +0 -0
  22. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/Dockerfile +0 -0
  23. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/__main__.py +0 -0
  24. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/core/__init__.py +0 -0
  25. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/core/config.py +0 -0
  26. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/core/discovery.py +0 -0
  27. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/core/state.py +0 -0
  28. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/core/storage.py +0 -0
  29. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/core/webhooks.py +0 -0
  30. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/plugins.py +0 -0
  31. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/spi.py +0 -0
  32. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/__init__.py +0 -0
  33. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/static/.gitkeep +0 -0
  34. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/static/style.css +0 -0
  35. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/templates/.gitkeep +0 -0
  36. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/templates/authorize.html +0 -0
  37. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/templates/base.html +0 -0
  38. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/templates/components/dashboard_payment_card.html +0 -0
  39. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/templates/components/payment_card.html +0 -0
  40. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/templates/components/status_badge.html +0 -0
  41. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/src/getpaid_simulator/ui/templates/dashboard.html +0 -0
  42. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/__init__.py +0 -0
  43. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/conftest.py +0 -0
  44. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/e2e/__init__.py +0 -0
  45. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/e2e/test_e2e_dashboard.py +0 -0
  46. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/e2e/test_e2e_payu_flow.py +0 -0
  47. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_cli.py +0 -0
  48. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_config.py +0 -0
  49. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_discovery.py +0 -0
  50. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_legacy_provider_modules.py +0 -0
  51. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_paynow_payments.py +0 -0
  52. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_paynow_refunds.py +0 -0
  53. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_paynow_signing.py +0 -0
  54. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_paynow_webhooks.py +0 -0
  55. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_payu_lifecycle.py +0 -0
  56. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_payu_oauth.py +0 -0
  57. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_payu_orders.py +0 -0
  58. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_payu_refunds.py +0 -0
  59. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_payu_webhooks.py +0 -0
  60. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_state.py +0 -0
  61. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_storage.py +0 -0
  62. {python_getpaid_simulator-3.0.0a3 → python_getpaid_simulator-3.0.0a4}/tests/test_webhooks.py +0 -0
@@ -0,0 +1,55 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ unit:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.12", "3.13"]
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Set up Python ${{ matrix.python-version }}
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install uv
24
+ run: pip install uv
25
+
26
+ - name: Install unit dependencies
27
+ run: uv sync --frozen
28
+
29
+ - name: Lint with ruff
30
+ run: uv run ruff check .
31
+
32
+ - name: Run unit tests
33
+ run: make test-unit
34
+
35
+ integration:
36
+ runs-on: ubuntu-latest
37
+ needs: unit
38
+ timeout-minutes: 30
39
+
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+
43
+ - name: Run Docker-backed integration tests
44
+ run: make test-integration
45
+
46
+ e2e:
47
+ runs-on: ubuntu-latest
48
+ needs: unit
49
+ timeout-minutes: 30
50
+
51
+ steps:
52
+ - uses: actions/checkout@v4
53
+
54
+ - name: Run Docker-backed end-to-end tests
55
+ run: make test-e2e
@@ -0,0 +1,22 @@
1
+ **
2
+ !getpaid-core/
3
+ !getpaid-core/**
4
+ !getpaid-payu/
5
+ !getpaid-payu/**
6
+ !getpaid-paynow/
7
+ !getpaid-paynow/**
8
+ !getpaid-simulator/
9
+ !getpaid-simulator/**
10
+
11
+ **/.git
12
+ **/__pycache__
13
+ **/.pytest_cache
14
+ **/.ruff_cache
15
+ **/.mypy_cache
16
+ **/.venv
17
+ **/.sisyphus
18
+ **/dist
19
+ **/build
20
+ **/*.pyc
21
+ **/*.pyo
22
+ **/*.egg-info
@@ -0,0 +1,39 @@
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app/getpaid-simulator
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ git \
7
+ libnss3 \
8
+ libnspr4 \
9
+ libdbus-1-3 \
10
+ libatk1.0-0 \
11
+ libatk-bridge2.0-0 \
12
+ libcups2 \
13
+ libdrm2 \
14
+ libxkbcommon0 \
15
+ libatspi2.0-0 \
16
+ libxcomposite1 \
17
+ libxdamage1 \
18
+ libxfixes3 \
19
+ libxrandr2 \
20
+ libgbm1 \
21
+ libpango-1.0-0 \
22
+ libcairo2 \
23
+ libasound2 \
24
+ libwayland-client0 \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ RUN pip install --no-cache-dir uv
28
+
29
+ COPY getpaid-core/ /app/getpaid-core/
30
+ COPY getpaid-payu/ /app/getpaid-payu/
31
+ COPY getpaid-paynow/ /app/getpaid-paynow/
32
+ COPY getpaid-simulator/ /app/getpaid-simulator/
33
+
34
+ ENV PLAYWRIGHT_BROWSERS_PATH="/app/getpaid-simulator/.cache/ms-playwright"
35
+
36
+ RUN uv sync --frozen --group e2e
37
+ RUN uv run playwright install chromium
38
+
39
+ ENV PATH="/app/getpaid-simulator/.venv/bin:$PATH"
@@ -0,0 +1,22 @@
1
+ **
2
+ !getpaid-core/
3
+ !getpaid-core/**
4
+ !getpaid-payu/
5
+ !getpaid-payu/**
6
+ !getpaid-paynow/
7
+ !getpaid-paynow/**
8
+ !getpaid-simulator/
9
+ !getpaid-simulator/**
10
+
11
+ **/.git
12
+ **/__pycache__
13
+ **/.pytest_cache
14
+ **/.ruff_cache
15
+ **/.mypy_cache
16
+ **/.venv
17
+ **/.sisyphus
18
+ **/dist
19
+ **/build
20
+ **/*.pyc
21
+ **/*.pyo
22
+ **/*.egg-info
@@ -0,0 +1,50 @@
1
+ .PHONY: test test-unit test-integration test-e2e test-build test-down
2
+
3
+ UNIT_TESTS = \
4
+ tests/test_cli.py \
5
+ tests/test_config.py \
6
+ tests/test_discovery.py \
7
+ tests/test_smoke.py \
8
+ tests/test_storage.py \
9
+ tests/test_state.py \
10
+ tests/test_app.py \
11
+ tests/test_legacy_provider_modules.py \
12
+ tests/test_test_infrastructure.py
13
+
14
+ INTEGRATION_TESTS = \
15
+ tests/test_ui_dashboard.py \
16
+ tests/test_ui_authorize.py \
17
+ tests/test_payu_oauth.py \
18
+ tests/test_payu_orders.py \
19
+ tests/test_payu_webhooks.py \
20
+ tests/test_payu_refunds.py \
21
+ tests/test_payu_lifecycle.py \
22
+ tests/test_paynow_signing.py \
23
+ tests/test_paynow_payments.py \
24
+ tests/test_paynow_webhooks.py \
25
+ tests/test_paynow_refunds.py \
26
+ tests/test_webhooks.py
27
+
28
+ E2E_TESTS = \
29
+ tests/e2e/test_e2e_dashboard.py \
30
+ tests/e2e/test_e2e_payu_flow.py
31
+
32
+ test-unit:
33
+ uv run pytest $(UNIT_TESTS) -x
34
+
35
+ test-integration: test-build
36
+ docker compose -f compose.test.yml run --rm tests uv run pytest $(INTEGRATION_TESTS) -x
37
+
38
+ test-e2e: test-build
39
+ docker compose -f compose.test.yml run --rm tests uv run pytest $(E2E_TESTS) -x
40
+
41
+ test:
42
+ $(MAKE) test-unit
43
+ $(MAKE) test-integration
44
+ $(MAKE) test-e2e
45
+
46
+ test-build:
47
+ docker compose -f compose.test.yml build
48
+
49
+ test-down:
50
+ docker compose -f compose.test.yml down -v
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-getpaid-simulator
3
- Version: 3.0.0a3
3
+ Version: 3.0.0a4
4
4
  Summary: Payment gateway simulator for testing the python-getpaid ecosystem.
5
5
  Project-URL: Homepage, https://github.com/django-getpaid/python-getpaid-simulator
6
6
  Project-URL: Repository, https://github.com/django-getpaid/python-getpaid-simulator
@@ -96,7 +96,7 @@ Example:
96
96
  ```toml
97
97
  [project.optional-dependencies]
98
98
  simulator = [
99
- "python-getpaid-simulator>=3.0.0a3",
99
+ "python-getpaid-simulator>=3.0.0a4",
100
100
  "litestar>=2.0",
101
101
  ]
102
102
  ```
@@ -73,7 +73,7 @@ Example:
73
73
  ```toml
74
74
  [project.optional-dependencies]
75
75
  simulator = [
76
- "python-getpaid-simulator>=3.0.0a3",
76
+ "python-getpaid-simulator>=3.0.0a4",
77
77
  "litestar>=2.0",
78
78
  ]
79
79
  ```
@@ -0,0 +1,30 @@
1
+ services:
2
+ testdb:
3
+ image: postgres:16
4
+ environment:
5
+ POSTGRES_DB: test_db
6
+ POSTGRES_USER: test_user
7
+ POSTGRES_PASSWORD: test_password
8
+ healthcheck:
9
+ test: ["CMD-SHELL", "pg_isready -U test_user -d test_db"]
10
+ interval: 2s
11
+ timeout: 5s
12
+ retries: 10
13
+ networks:
14
+ - test-net
15
+
16
+ tests:
17
+ build:
18
+ context: ..
19
+ dockerfile: getpaid-simulator/Dockerfile.test
20
+ depends_on:
21
+ testdb:
22
+ condition: service_healthy
23
+ environment:
24
+ TEST_DATABASE_URL: postgresql://test_user:test_password@testdb:5432/test_db
25
+ networks:
26
+ - test-net
27
+
28
+ networks:
29
+ test-net:
30
+ driver: bridge
@@ -1,5 +1,3 @@
1
- version: "3.9"
2
-
3
1
  services:
4
2
  simulator:
5
3
  build:
@@ -12,7 +10,7 @@ services:
12
10
  - SIMULATOR_PORT=9000
13
11
  - SIMULATOR_LOG_LEVEL=info
14
12
  healthcheck:
15
- test: ["CMD", "curl", "-f", "http://localhost:9000/sim/dashboard"]
13
+ test: ["CMD", "curl", "-f", "http://localhost:9000/sim/status"]
16
14
  interval: 10s
17
15
  timeout: 5s
18
16
  retries: 3
@@ -36,12 +36,13 @@ dev = [
36
36
  'litestar[testing]',
37
37
  'respx>=0.21.0',
38
38
  "playwright>=1.40.0",
39
- 'python-getpaid-payu>=3.0.0a3',
40
- 'python-getpaid-paynow>=3.0.0a3',
39
+ 'python-getpaid-core>=3.0.0a4',
40
+ 'python-getpaid-payu>=3.0.0a4',
41
+ 'python-getpaid-paynow>=3.0.0a4',
41
42
  ]
42
43
  e2e = [
43
44
  {include-group = "dev"},
44
- 'python-getpaid-core>=3.0.0a3',
45
+ 'python-getpaid-core>=3.0.0a4',
45
46
  'python-getpaid-payu',
46
47
  'python-getpaid-paynow',
47
48
  ]
@@ -1,3 +1,3 @@
1
1
  """Payment gateway simulator for testing the python-getpaid ecosystem."""
2
2
 
3
- __version__ = "3.0.0a3"
3
+ __version__ = "3.0.0a4"
@@ -9,6 +9,7 @@ from litestar import Litestar
9
9
  from litestar import get
10
10
  from litestar.contrib.jinja import JinjaTemplateEngine
11
11
  from litestar.datastructures import State
12
+ from litestar.static_files import create_static_files_router
12
13
  from litestar.template.config import TemplateConfig
13
14
 
14
15
  from getpaid_simulator.core.config import SimulatorConfig
@@ -115,9 +116,13 @@ def create_app(
115
116
  "invalid_transition_error": InvalidTransitionError,
116
117
  }
117
118
  )
119
+ static_files_router = create_static_files_router(
120
+ path="/static",
121
+ directories=[Path(__file__).parent / "ui" / "static"],
122
+ )
118
123
 
119
124
  return Litestar(
120
- route_handlers=route_handlers,
125
+ route_handlers=[*route_handlers, static_files_router],
121
126
  state=state,
122
127
  template_config=TemplateConfig(
123
128
  engine=JinjaTemplateEngine(
@@ -125,6 +130,3 @@ def create_app(
125
130
  )
126
131
  ),
127
132
  )
128
-
129
-
130
- app = create_app()
@@ -1,3 +1,6 @@
1
+ from decimal import Decimal
2
+ from decimal import InvalidOperation
3
+ from typing import Any
1
4
  from typing import Optional
2
5
 
3
6
  from litestar import get
@@ -9,6 +12,43 @@ from getpaid_simulator.plugins import ProviderLoadFailure
9
12
  from getpaid_simulator.spi import SimulatorProviderPlugin
10
13
 
11
14
 
15
+ def _format_amount_for_display(
16
+ order: dict[str, Any],
17
+ provider_config: dict[str, Any] | None,
18
+ ) -> str:
19
+ amount_raw = order.get("amount", order.get("totalAmount", 0))
20
+ currency = str(order.get("currency", order.get("currencyCode", "PLN")))
21
+ try:
22
+ amount_value = Decimal(str(amount_raw))
23
+ except (InvalidOperation, TypeError, ValueError):
24
+ return str(amount_raw)
25
+
26
+ minor_unit_places = _minor_unit_places(provider_config)
27
+ if minor_unit_places is not None:
28
+ amount_value /= Decimal(10) ** minor_unit_places
29
+
30
+ return f"{amount_value:.2f} {currency}"
31
+
32
+
33
+ def _minor_unit_places(provider_config: dict[str, Any] | None) -> int | None:
34
+ if provider_config is None:
35
+ return None
36
+
37
+ raw_value = provider_config.get("amount_minor_unit_places")
38
+ if raw_value is None:
39
+ return None
40
+
41
+ try:
42
+ places = int(raw_value)
43
+ except (TypeError, ValueError):
44
+ return None
45
+
46
+ if places < 0:
47
+ return None
48
+
49
+ return places
50
+
51
+
12
52
  @get(["/sim/", "/sim/dashboard"])
13
53
  async def dashboard(state: State, provider: Optional[str] = None) -> Template:
14
54
  """Render payments dashboard."""
@@ -30,16 +70,9 @@ async def dashboard(state: State, provider: Optional[str] = None) -> Template:
30
70
 
31
71
  orders = []
32
72
  for order in orders_data:
33
- amount_raw = order.get("amount", order.get("totalAmount", 0))
34
- currency = order.get("currency", order.get("currencyCode", "PLN"))
35
- try:
36
- val = float(amount_raw) / 100
37
- formatted = f"{val:.2f} {currency}"
38
- except (ValueError, TypeError):
39
- formatted = str(amount_raw)
40
-
41
73
  provider_slug = str(order.get("provider", "unknown"))
42
74
  plugin = loaded_plugins.get(provider_slug)
75
+ provider_config = state.provider_configs.get(provider_slug)
43
76
  orders.append(
44
77
  {
45
78
  "id": order["id"],
@@ -48,7 +81,10 @@ async def dashboard(state: State, provider: Optional[str] = None) -> Template:
48
81
  plugin.display_name if plugin is not None else provider_slug
49
82
  ),
50
83
  "status": order.get("status", "NEW"),
51
- "formatted_amount": formatted,
84
+ "formatted_amount": _format_amount_for_display(
85
+ order,
86
+ provider_config,
87
+ ),
52
88
  "authorize_url": (
53
89
  plugin.build_authorize_path(str(order["id"]))
54
90
  if plugin is not None
@@ -1,9 +1,12 @@
1
1
  """E2E test configuration with Playwright and live server."""
2
2
 
3
3
  import asyncio
4
+ import os
4
5
  import socket
5
6
  from collections.abc import AsyncGenerator
6
7
  from typing import Any
8
+ from typing import NotRequired
9
+ from typing import TypedDict
7
10
  from urllib.parse import urlsplit
8
11
 
9
12
  import httpx
@@ -16,6 +19,27 @@ from playwright.async_api import async_playwright
16
19
  from getpaid_simulator.app import create_app
17
20
 
18
21
 
22
+ class ChromiumLaunchKwargs(TypedDict):
23
+ headless: bool
24
+ args: list[str]
25
+ executable_path: NotRequired[str]
26
+
27
+
28
+ def get_chromium_launch_kwargs() -> ChromiumLaunchKwargs:
29
+ kwargs: ChromiumLaunchKwargs = {
30
+ "headless": True,
31
+ "args": [
32
+ "--no-sandbox",
33
+ "--disable-dev-shm-usage",
34
+ "--disable-gpu",
35
+ ],
36
+ }
37
+ executable_path = os.getenv("PLAYWRIGHT_CHROMIUM_EXECUTABLE")
38
+ if executable_path:
39
+ kwargs["executable_path"] = executable_path
40
+ return kwargs
41
+
42
+
19
43
  def _find_free_port() -> int:
20
44
  """Find a free port for the test server."""
21
45
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -66,17 +90,9 @@ async def live_server() -> AsyncGenerator[str, None]:
66
90
 
67
91
  @pytest.fixture
68
92
  async def browser() -> AsyncGenerator[Browser, None]:
69
- """Playwright browser instance with system Chromium."""
93
+ """Playwright browser instance for E2E tests."""
70
94
  async with async_playwright() as p:
71
- browser = await p.chromium.launch(
72
- headless=True,
73
- executable_path="/usr/bin/chromium",
74
- args=[
75
- "--no-sandbox",
76
- "--disable-dev-shm-usage",
77
- "--disable-gpu",
78
- ],
79
- )
95
+ browser = await p.chromium.launch(**get_chromium_launch_kwargs())
80
96
  yield browser
81
97
  await browser.close()
82
98
 
@@ -158,3 +158,61 @@ async def test_dashboard_renders_dynamic_filters_and_plugin_warning(
158
158
  body = response.text
159
159
  assert "FakePay" in body
160
160
  assert "brokenpay" in body
161
+
162
+
163
+ @pytest.mark.asyncio
164
+ async def test_create_app_serves_static_stylesheet(monkeypatch):
165
+ monkeypatch.setattr(
166
+ app_module,
167
+ "load_provider_plugins",
168
+ lambda config: PluginLoadResult(
169
+ loaded_plugins=(),
170
+ failed_plugins=(),
171
+ provider_configs={},
172
+ ),
173
+ )
174
+ app = app_module.create_app()
175
+
176
+ async with AsyncTestClient(app=app) as test_client:
177
+ response = await test_client.get("/static/style.css")
178
+
179
+ assert response.status_code == 200
180
+ assert "text/css" in response.headers["content-type"]
181
+
182
+
183
+ def test_importing_app_module_does_not_load_plugins(monkeypatch):
184
+ import importlib
185
+ import sys
186
+
187
+ sys.modules.pop("getpaid_simulator.app", None)
188
+ load_calls: list[object] = []
189
+
190
+ def fake_load_provider_plugins(config):
191
+ load_calls.append(config)
192
+ return PluginLoadResult(
193
+ loaded_plugins=(),
194
+ failed_plugins=(),
195
+ provider_configs={},
196
+ )
197
+
198
+ monkeypatch.setattr(
199
+ "getpaid_simulator.plugins.load_provider_plugins",
200
+ fake_load_provider_plugins,
201
+ )
202
+
203
+ app_module_reimported = importlib.import_module("getpaid_simulator.app")
204
+
205
+ assert load_calls == []
206
+ app_module_reimported.create_app()
207
+ assert len(load_calls) == 1
208
+
209
+
210
+ def test_docker_compose_healthcheck_uses_health_endpoint():
211
+ from pathlib import Path
212
+
213
+ compose_file = Path(__file__).resolve().parents[1] / "docker-compose.yml"
214
+ content = compose_file.read_text()
215
+
216
+ assert 'http://localhost:9000/"' not in content
217
+ assert "http://localhost:9000/sim/dashboard" not in content
218
+ assert "http://localhost:9000/sim/status" in content
@@ -0,0 +1,43 @@
1
+ """Smoke tests for getpaid-simulator."""
2
+
3
+ from pathlib import Path
4
+ import tomllib
5
+
6
+ import pytest
7
+
8
+ import getpaid_simulator
9
+
10
+
11
+ def test_version():
12
+ """Test that version is accessible."""
13
+ assert getpaid_simulator.__version__ == "3.0.0a4"
14
+
15
+
16
+ def test_e2e_core_dependency_floor():
17
+ """E2E dependency group requires the published core alpha."""
18
+ pyproject_data = tomllib.loads(Path("pyproject.toml").read_text())
19
+ assert (
20
+ "python-getpaid-core>=3.0.0a4"
21
+ in pyproject_data["dependency-groups"]["e2e"]
22
+ )
23
+
24
+
25
+ def test_dev_provider_dependency_floors():
26
+ """Simulator dev environment tracks published provider alpha floors."""
27
+ pyproject_data = tomllib.loads(Path("pyproject.toml").read_text())
28
+ dev_dependencies = pyproject_data["dependency-groups"]["dev"]
29
+ assert "python-getpaid-core>=3.0.0a4" in dev_dependencies
30
+ assert "python-getpaid-payu>=3.0.0a4" in dev_dependencies
31
+ assert "python-getpaid-paynow>=3.0.0a4" in dev_dependencies
32
+
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_health_endpoint(test_client):
36
+ """Test health endpoint returns OK."""
37
+ response = await test_client.get("/")
38
+ assert response.status_code == 200
39
+ body = response.json()
40
+ assert body["service"] == "getpaid-simulator"
41
+ assert body["status"] in {"ok", "degraded"}
42
+ assert isinstance(body["loadedProviders"], list)
43
+ assert isinstance(body["failedProviders"], list)
@@ -0,0 +1,102 @@
1
+ from pathlib import Path
2
+
3
+ from tests.e2e.conftest import get_chromium_launch_kwargs
4
+
5
+
6
+ REPO_ROOT = Path(__file__).resolve().parents[1]
7
+
8
+
9
+ def test_compose_test_uses_isolated_services_without_host_ports() -> None:
10
+ content = (REPO_ROOT / "compose.test.yml").read_text()
11
+
12
+ assert "testdb:" in content
13
+ assert "tests:" in content
14
+ assert "dockerfile: getpaid-simulator/Dockerfile.test" in content
15
+ assert "condition: service_healthy" in content
16
+ assert (
17
+ "TEST_DATABASE_URL: postgresql://test_user:test_password@testdb:5432/test_db"
18
+ in content
19
+ )
20
+ assert "ports:" not in content
21
+
22
+
23
+ def test_dockerfile_test_installs_e2e_dependencies() -> None:
24
+ content = (REPO_ROOT / "Dockerfile.test").read_text()
25
+
26
+ assert "FROM python:3.12-slim" in content
27
+ assert "COPY getpaid-core/ /app/getpaid-core/" in content
28
+ assert "COPY getpaid-payu/ /app/getpaid-payu/" in content
29
+ assert "COPY getpaid-paynow/ /app/getpaid-paynow/" in content
30
+ assert "COPY getpaid-simulator/ /app/getpaid-simulator/" in content
31
+ assert "uv sync --frozen --group e2e" in content
32
+ assert (
33
+ 'ENV PLAYWRIGHT_BROWSERS_PATH="/app/getpaid-simulator/.cache/ms-playwright"'
34
+ in content
35
+ )
36
+ assert "uv run playwright install chromium" in content
37
+ assert content.index("PLAYWRIGHT_BROWSERS_PATH") < content.index(
38
+ "uv run playwright install chromium"
39
+ )
40
+
41
+
42
+ def test_makefile_exposes_unit_integration_and_e2e_targets() -> None:
43
+ content = (REPO_ROOT / "Makefile").read_text()
44
+
45
+ assert (
46
+ ".PHONY: test test-unit test-integration test-e2e test-build test-down"
47
+ in content
48
+ )
49
+ assert "test-unit:" in content
50
+ assert "test-integration:" in content
51
+ assert "test-e2e:" in content
52
+ assert "docker compose -f compose.test.yml run --rm tests" in content
53
+
54
+
55
+ def test_dockerfile_specific_ignore_files_keep_only_required_repos() -> None:
56
+ for file_name in (
57
+ "Dockerfile.dockerignore",
58
+ "Dockerfile.test.dockerignore",
59
+ ):
60
+ content = (REPO_ROOT / file_name).read_text()
61
+
62
+ assert "**" in content
63
+ assert "!getpaid-core/" in content
64
+ assert "!getpaid-core/**" in content
65
+ assert "!getpaid-payu/" in content
66
+ assert "!getpaid-payu/**" in content
67
+ assert "!getpaid-paynow/" in content
68
+ assert "!getpaid-paynow/**" in content
69
+ assert "!getpaid-simulator/" in content
70
+ assert "!getpaid-simulator/**" in content
71
+ assert "**/.git" in content
72
+ assert "**/.venv" in content
73
+
74
+
75
+ def test_chromium_launch_uses_playwright_bundle_by_default(
76
+ monkeypatch,
77
+ ) -> None:
78
+ monkeypatch.delenv("PLAYWRIGHT_CHROMIUM_EXECUTABLE", raising=False)
79
+
80
+ kwargs = get_chromium_launch_kwargs()
81
+
82
+ assert kwargs == {
83
+ "headless": True,
84
+ "args": [
85
+ "--no-sandbox",
86
+ "--disable-dev-shm-usage",
87
+ "--disable-gpu",
88
+ ],
89
+ }
90
+
91
+
92
+ def test_chromium_launch_allows_explicit_executable_override(
93
+ monkeypatch,
94
+ ) -> None:
95
+ monkeypatch.setenv(
96
+ "PLAYWRIGHT_CHROMIUM_EXECUTABLE",
97
+ "/usr/bin/chromium",
98
+ )
99
+
100
+ kwargs = get_chromium_launch_kwargs()
101
+
102
+ assert kwargs.get("executable_path") == "/usr/bin/chromium"
@@ -40,6 +40,32 @@ async def test_payu_authorize_get(test_client, simulator_storage):
40
40
  assert "Reject" in response.text
41
41
 
42
42
 
43
+ @pytest.mark.asyncio
44
+ async def test_payu_authorize_get_uses_provider_amount_minor_unit_places(
45
+ test_client,
46
+ simulator_storage,
47
+ ):
48
+ test_client.app.state.provider_configs["payu"][
49
+ "amount_minor_unit_places"
50
+ ] = 0
51
+ order_id = simulator_storage.create_order(
52
+ {
53
+ "provider": "payu",
54
+ "totalAmount": "1000",
55
+ "currencyCode": "PLN",
56
+ "description": "Test order",
57
+ "continueUrl": "https://example.com/continue",
58
+ "notifyUrl": "https://example.com/notify",
59
+ }
60
+ )
61
+
62
+ response = await test_client.get(f"/sim/payu/authorize/{order_id}")
63
+
64
+ assert response.status_code == 200
65
+ assert "1000.00 PLN" in response.text
66
+ assert "10.00 PLN" not in response.text
67
+
68
+
43
69
  @pytest.mark.asyncio
44
70
  async def test_payu_authorize_get_404(test_client):
45
71
  response = await test_client.get("/sim/payu/authorize/non-existent-order")
@@ -116,6 +142,31 @@ async def test_paynow_authorize_get(test_client, simulator_storage):
116
142
  assert "PayNow" in response.text
117
143
 
118
144
 
145
+ @pytest.mark.asyncio
146
+ async def test_paynow_authorize_get_uses_provider_amount_minor_unit_places(
147
+ test_client,
148
+ simulator_storage,
149
+ ):
150
+ test_client.app.state.provider_configs["paynow"][
151
+ "amount_minor_unit_places"
152
+ ] = 0
153
+ payment_id = simulator_storage.create_order(
154
+ {
155
+ "provider": "paynow",
156
+ "amount": 1000,
157
+ "currency": "PLN",
158
+ "description": "Test PayNow",
159
+ "continueUrl": "https://example.com/paynow-continue",
160
+ }
161
+ )
162
+
163
+ response = await test_client.get(f"/sim/paynow/authorize/{payment_id}")
164
+
165
+ assert response.status_code == 200
166
+ assert "1000.00 PLN" in response.text
167
+ assert "10.00 PLN" not in response.text
168
+
169
+
119
170
  @pytest.mark.asyncio
120
171
  async def test_paynow_authorize_post_approve(test_client, simulator_storage):
121
172
  payment_id = simulator_storage.create_order(
@@ -68,6 +68,45 @@ async def test_dashboard_filter_by_provider(test_client, simulator_storage):
68
68
  assert f"/sim/payu/authorize/{order1_id}" not in html2
69
69
 
70
70
 
71
+ @pytest.mark.asyncio
72
+ async def test_dashboard_uses_provider_amount_minor_unit_places(
73
+ test_client,
74
+ simulator_storage,
75
+ ):
76
+ test_client.app.state.provider_configs["payu"][
77
+ "amount_minor_unit_places"
78
+ ] = 0
79
+ simulator_storage.create_order(
80
+ {"totalAmount": "1500", "currencyCode": "PLN", "description": "test"},
81
+ provider="payu",
82
+ )
83
+
84
+ response = await test_client.get("/sim/dashboard?provider=payu")
85
+
86
+ assert response.status_code == 200
87
+ html = response.text
88
+ assert "1500.00 PLN" in html
89
+ assert "15.00 PLN" not in html
90
+
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_dashboard_falls_back_to_raw_units_for_unknown_provider(
94
+ test_client,
95
+ simulator_storage,
96
+ ):
97
+ simulator_storage.create_order(
98
+ {"amount": "12.50", "currency": "PLN", "description": "custom"},
99
+ provider="custompay",
100
+ )
101
+
102
+ response = await test_client.get("/sim/dashboard?provider=custompay")
103
+
104
+ assert response.status_code == 200
105
+ html = response.text
106
+ assert "12.50 PLN" in html
107
+ assert "0.12 PLN" not in html
108
+
109
+
71
110
  @pytest.mark.asyncio
72
111
  async def test_dashboard_redirects_from_root(test_client, simulator_storage):
73
112
  response = await test_client.get("/sim/")
@@ -1,22 +0,0 @@
1
- """Smoke tests for getpaid-simulator."""
2
-
3
- import pytest
4
-
5
- import getpaid_simulator
6
-
7
-
8
- def test_version():
9
- """Test that version is accessible."""
10
- assert getpaid_simulator.__version__ == "3.0.0a3"
11
-
12
-
13
- @pytest.mark.asyncio
14
- async def test_health_endpoint(test_client):
15
- """Test health endpoint returns OK."""
16
- response = await test_client.get("/")
17
- assert response.status_code == 200
18
- body = response.json()
19
- assert body["service"] == "getpaid-simulator"
20
- assert body["status"] in {"ok", "degraded"}
21
- assert isinstance(body["loadedProviders"], list)
22
- assert isinstance(body["failedProviders"], list)