Skip to content

QA Test Framework: Architecture

This document provides a deep dive into the technical architecture of the QA test framework. It is intended for developers who want to understand the core design, patterns, and advanced concepts that underpin the test suite.

For practical instructions on setup, writing tests, and daily workflow, please see the Developer Guide.


Table of Contents


Framework Overview

Component Layers

flowchart TD
    subgraph Features["Feature Files Layer - Gherkin BDD"]
        F1[access_test.feature]
        F2[ftmo_eval_global.feature]
        F3[oanda_prop_trading.feature]
    end

    subgraph Steps["Step Definitions Layer"]
        S1[authentication_steps.py]
        S2[document_steps.py]
        S3[verification_steps.py]
    end

    subgraph Business["Business Logic Layer"]
        B1[ImportConfig]
        B2[DocumentBase]
        B3[TranslationHandler]
    end

    subgraph Helpers["Helper Layer"]
        H1[Money]
        H2[ExchangeRates]
        H3[DateHelper]
        H4[GCSHelper]
    end

    subgraph Models["Model Layer"]
        M1[Invoice<br/>50+ Helios fields]
        M2[Organization]
        M3[DocumentMetadata]
    end

    subgraph Data["Data Layer"]
        D1[Enums<br/>Currency, DatabaseSetting]
        D2[Test Data<br/>Payment info]
        D3[Test Users<br/>50+ users]
    end

    subgraph External["External Systems"]
        E1[Helios<br/>Czech]
        E2[NetSuite<br/>US]
        E3[Xero<br/>Australia]
        E4[GCS<br/>Oanda]
    end

    Features --> Steps
    Steps --> Business
    Business --> Helpers
    Helpers --> Models
    Models --> Data
    Data --> External

Test Execution Flow

flowchart TD
    A[1. Behave starts] --> B[2. environment.py - before_all<br/>Load config, setup logging,<br/>load users, initialize controllers]
    B --> C[3. Feature file parsed]
    C --> D[4. authentication_steps.py<br/>Set import_user & user,<br/>create controllers]
    D --> E[5. document_steps.py<br/>Parse parameters -- ImportConfig<br/>Build import request 50+ fields<br/>Call _import_document_with_retry]
    E --> F{6. Retry Logic<br/>max 3 attempts}
    F -->|201| G[Success]
    F -->|409| H[Regenerate timestamp<br/>Retry]
    F -->|429| I[Exponential backoff<br/>Retry]
    H --> F
    I --> F
    G --> J[7. verification_steps.py<br/>Assert status code]
    J --> K[8. document_steps.py validation<br/>GET document from API<br/>Create AccountingDocumentBase<br/>deepdiff_compare]
    K --> L[9. Test Result<br/>Pass/Fail]

Framework Principles

  1. Separation of Concerns - Clear boundaries between layers
  2. Single Responsibility - Each component has one job
  3. DRY (Don't Repeat Yourself) - Shared logic in helpers
  4. Testability - All components are testable independently
  5. Extensibility - Easy to add new platforms, countries, item types
  6. Type Safety - Pydantic models provide runtime validation
  7. Immutability - Prefer immutable data structures where possible

Directory Structure

graph TD
    A["qa/"] --> B["config.py<br/>Configuration management"]
    A --> C["environment.py<br/>Behave hooks"]
    A --> D["logger.py<br/>Logging setup"]
    A --> E["makefile<br/>Build automation"]
    A --> F["pyproject.toml<br/>Dependencies"]
    A --> G["DEVELOPER_GUIDE.md<br/>Daily guide"]
    A --> H["ARCHITECTURE.md<br/>Technical reference"]

    A --> I["enums/<br/>Constants"]
    I --> I1["enums_currency.py<br/>21 currencies + rates"]
    I --> I2["enums_db_setting.py<br/>Database &rarr; Platform"]
    I --> I3["enums_vat_rate.py<br/>VAT rates by country"]

    A --> J["features/<br/>Gherkin scenarios"]
    J --> J1["access_test.feature"]
    J --> J2["ftmo_eval_global.feature"]

    A --> K["helpers/<br/>Utilities"]
    K --> K1["money.py<br/>Currency operations"]
    K --> K2["exchange_rates.py<br/>Static rates"]
    K --> K3["document_base.py<br/>Document validation"]

    A --> L["model/<br/>Pydantic models"]
    L --> L1["import_config.py<br/>Test configuration"]
    L --> L2["document_import.py<br/>Invoice 50+ fields"]
    L --> L3["organization_data.py<br/>Customer data"]

    A --> M["steps/<br/>Step definitions"]
    M --> M1["authentication_steps.py<br/>Auth setup"]
    M --> M2["document_steps.py<br/>Document ops"]
    M --> M3["verification_steps.py<br/>Assertions"]

    A --> N["test_data/<br/>Test data"]
    N --> N1["README.md<br/>Data guide"]
    N --> N2["test_users.dev.yaml<br/>50+ users"]
    N --> N3["payment_info_*.json<br/>Payment data"]

Module Index

Configuration

  • config.py - Settings management
  • environment.py - Behave hooks (setup/teardown)
  • logger.py - Logging configuration

Enums

  • enums_currency.py - 21 currencies with exchange rates
  • enums_db_setting.py - Database → Platform mapping
  • enums_company.py - FTMO entities (IDs 5, 6)
  • enums_vat_rate.py - VAT rates by country
  • enums_item_types.py - Invoice item types
  • enums_regions.py - Countries, states, tax regimes
  • enums_bank_account.py - Bank account details
  • enums_labels.py - UI labels and translations
  • enums_ar_translations.py - Arabic translations

Helpers

  • money.py - Currency operations (Money class)
  • exchange_rates.py - Static exchange rates (Singleton)
  • date_helper.py - Date/timezone operations
  • document_base.py - Document preparation/validation
  • document_helper.py - Utility functions
  • gcs_helper.py - Google Cloud Storage operations
  • file_helper.py - Test data loading
  • transformation_helper.py - Data transformation utilities
  • translation_handler.py - Multi-language support

Models

  • base_model.py - Base models (MixedCase, KebabCase)
  • import_config.py - Test configuration
  • document_import.py - Invoice (50+ Helios fields)
  • document_item.py - Parsed invoice items
  • document_metadata.py - Payment info (22 fields)
  • organization_data.py - Organization/customer data
  • document_html.py - HTML response models
  • document_json.py - JSON response models
  • responses.py - Response model exports

Steps

  • init.py - Package overview and step index
  • authentication_steps.py - Token setup (2 steps)
  • document_steps.py - Document operations (15+ steps)
  • verification_steps.py - Status code validation (1 step)

Architectural Patterns

1. Layered Architecture

The framework follows a strict layered architecture:

┌─────────────────────────────────────────────────┐
│         Presentation Layer (BDD)                │
│  - Feature files (.feature)                     │
│  - Gherkin scenarios                            │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│         Step Definition Layer                    │
│  - Step implementations                          │
│  - Context management                            │
│  - Orchestration logic                           │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│         Business Logic Layer                     │
│  - ImportConfig (document builder)               │
│  - AccountingDocumentBase (expectations)         │
│  - Helper functions                              │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│         Model Layer                              │
│  - Pydantic models                               │
│  - Enumerations                                  │
│  - Data validation                               │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│         Service Layer                            │
│  - HTTP clients (Controller)                     │
│  - Authentication (AuthController)               │
│  - External integrations                         │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│         External Systems                         │
│  - Accounting Service API                        │
│  - Keycloak                                      │
│  - Database (indirect)                           │
└─────────────────────────────────────────────────┘

Layer Responsibilities:

  • Presentation: Human-readable test scenarios
  • Step Definitions: Glue between Gherkin and code
  • Business Logic: Core test logic and document manipulation
  • Model: Data structures and validation
  • Service: External communication
  • External Systems: APIs and third-party services

2. Factory Pattern

Purpose: Create objects without specifying exact class

Implementation:

class TranslationFactory:
  """Factory for creating translation models based on country"""

  @classmethod
  def get_translation_model(cls, vat_country: str) -> Type[BaseModel] | None:
    """
    Returns appropriate translation model for country.

    Args:
        vat_country: ISO country code (AE, SA, OM)

    Returns:
        Translation model class or None
    """
    match vat_country:
      case "AE" | "SA" | "OM":
        return ArabicInvoiceModel
      case _:
        return None

  @classmethod
  def get_provider(cls, country_code: str) -> TranslationProvider:
    """Get translation provider for specific country"""
    return cls._providers.get(country_code, NoTranslationProvider())

Benefits:

  • Decouples client code from concrete classes
  • Easy to add new translation models
  • Centralized object creation logic

3. Builder Pattern

Purpose: Construct complex objects step by step

Implementation: ImportConfig class

class ImportConfig:
  """Builder for document import requests"""

  def __init__(self, init_data: dict):
    """Initialize with table data from feature file"""
    self._init = ImportConfigInitModel(**init_data)
    self._setup_derived_attributes()

  def build_import_document(self, user_data: User) -> dict:
    """
    Build complete import document structure.

    Returns:
        Complete document JSON ready for API
    """
    self.document_body = {
      "init": self._init_db_data,
      "invoice": [],
      "organization": self._organization_data,
    }

    # Build main invoice item
    main_item = self._create_invoice_item()

    # Add metadata if present
    if self._init.payment_info:
      main_item["Metadata"] = self._payment_info

    self.document_body["invoice"].append(main_item)

    # Add additional items (points, discounts, etc.)
    self._add_additional_items()

    return self.document_body

Benefits:

  • Complex construction logic isolated
  • Step-by-step building with validation
  • Reusable for different document types

4. Strategy Pattern

Purpose: Define family of algorithms, encapsulate each one

Implementation: Platform-specific handling

class ItemType(Enum):
  """Item type with platform-specific strategies"""

  def account_number(self, db_setting: DatabaseSetting) -> str:
    """Get account number based on platform strategy"""
    match db_setting.platform:
      case Platform.HELIOS:
        return self._helios_account_number()
      case Platform.XERO:
        return self._xero_account_number()
      case Platform.NETSUITE:
        return self._netsuite_account_number()
      case _:
        raise NotImplementedError(f"Platform {db_setting.platform}")

  def _helios_account_number(self) -> str:
    """Helios-specific account number logic"""
    return CisloUcetPol.get_helios_account_number(self)

  def _xero_account_number(self) -> str:
    """Xero-specific account number logic"""
    return self.value.xero_type

  def _netsuite_account_number(self) -> str:
    """NetSuite-specific account number logic"""
    return self.value.code

Benefits:

  • Platform-specific logic encapsulated
  • Easy to add new platforms
  • Runtime platform selection

5. Adapter Pattern

Purpose: Convert interface to expected interface

Implementation: MixedCaseModel

class MixedCaseModel(BaseModel):
  """
  Adapter for Pydantic models with PascalCase aliases.
  Converts Python snake_case to JSON PascalCase.
  """

  model_config = ConfigDict(
    alias_generator=to_pascal,  # Convert field names
    populate_by_name=True,  # Accept both formats
    str_strip_whitespace=True,  # Clean strings
    validate_assignment=True,  # Validate on change
  )

  def compare(self, other: "MixedCaseModel", **kwargs) -> dict:
    """Compare two models using DeepDiff"""
    return DeepDiff(
      self.model_dump(by_alias=True),
      other.model_dump(by_alias=True),
      **kwargs
    )

Benefits:

  • Bridges Python conventions with API requirements
  • Automatic case conversion
  • Consistent interface across models

6. Repository Pattern

Purpose: Encapsulate data access logic

Implementation: AuthController as user repository

class AuthController:
  """Repository for user data and authentication"""

  def __init__(self, users: dict, kc_base_url: str, default_client: str):
    self.users = users  # User repository
    self._kc_base_url = kc_base_url
    self._default_client = default_client
    self._token_cache: dict[str, Token] = {}

  def get_token(self, user_id: str, client_id: str) -> Token:
    """
    Retrieve token from cache or generate new one.

    Repository pattern: Abstracts token storage/retrieval.
    """
    cache_key = f"{user_id}:{client_id}"

    if cache_key in self._token_cache:
      token = self._token_cache[cache_key]
      if not token.is_expired():
        return token

    # Generate new token
    token = self._fetch_token_from_keycloak(user_id, client_id)
    self._token_cache[cache_key] = token
    return token

Benefits:

  • Centralized data access
  • Caching strategy encapsulated
  • Easy to change data source

Advanced Topics

This section covers advanced architectural concepts, design decisions, and strategies for extending the framework.

Core Components

Data & Service Layers

Strategy & Design


1. Context Management

File: environment.py

Purpose: Behave hooks for test lifecycle management

def before_all(context: Context) -> None:
  """
  Initialize global test context before any tests run.

  Responsibilities:
  - Load configuration
  - Initialize logging
  - Create service controllers
  - Setup authentication
  """
  context.settings = AccountingServiceTestConfig()
  configure_logging(context.settings.log_level)

  auth_controller = AuthController(
    users=context.settings.users,
    kc_base_url=StaticConfig.KEYCLOAK_BASE_URL,
    default_client=context.settings.users.default_client,
  )

  context.controller = Controller(
    auth_controller=auth_controller,
    base_url=context.settings.base_url,
  )


def before_feature(context: Context, feature: Feature) -> None:
  """Initialize feature-level context"""
  context.users = context.controller.auth.users


def before_scenario(context: Context, scenario: Scenario) -> None:
  """Initialize scenario-level context"""
  context.sent_import_data = None
  context.import_config = None
  context.imported_documents = []
  context.gcs_blobs_to_delete = []


def after_scenario(context: Context, scenario: Scenario) -> None:
  """Cleanup after scenario"""
  for bucket_name, blob_name in getattr(context, "gcs_blobs_to_delete", []):
    delete_gcs_blob(bucket_name, blob_name)

Context Lifecycle:

  1. before_all - Global setup (once per test run)
  2. before_feature - Feature setup (per .feature file)
  3. before_scenario - Scenario setup (per test)
  4. [Test execution]
  5. after_scenario - Scenario cleanup
  6. after_feature - Feature cleanup (if defined)
  7. after_all - Global cleanup (if defined)

2. Configuration Management

File: config.py

Architecture:

class StaticConfig:
  """Static configuration from environment variables"""

  KEYCLOAK_BASE_URL = os.getenv("KEYCLOAK_BASE_URL")
  LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")

  @classmethod
  def validate(cls) -> None:
    """Validate all required config is present"""
    required = ["KEYCLOAK_BASE_URL"]
    missing = [k for k in required if not getattr(cls, k)]
    if missing:
      raise ConfigurationError(f"Missing: {missing}")


class AccountingServiceTestConfig:
  """Runtime configuration with validation"""

  def __init__(self):
    self.base_url = os.getenv("BASE_URL")
    self.timeout = int(os.getenv("TIMEOUT", "30"))
    self.log_level = os.getenv("LOG_LEVEL", "INFO")

    # Load users from secrets manager or config
    self.users = self._load_users()

    # Validate configuration
    self._validate()

  def _validate(self) -> None:
    """Validate configuration is complete"""
    if not self.base_url:
      raise ConfigurationError("BASE_URL not set")

Configuration Layers:

  1. Environment Variables - Override everything
  2. Config Files - Default values
  3. Secrets Manager - Sensitive data (if integrated)
  4. Hard-coded Defaults - Fallback values

3. Logger Architecture

File: logger.py

Design:

def configure_logging(log_level: str = "INFO") -> None:
  """
  Configure logging with custom format and handlers.

  Logger hierarchy:
  - root: Catches all
  - steps_logger: Step definitions
  - http_logger: HTTP requests
  - validation_logger: Validation
  """
  logging.basicConfig(
    level=getattr(logging, log_level.upper()),
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
      logging.FileHandler('logs/test_execution.log'),
      logging.StreamHandler(sys.stdout)
    ]
  )

  # Configure specific loggers
  logging.getLogger('urllib3').setLevel(logging.WARNING)
  logging.getLogger('requests').setLevel(logging.WARNING)


# Specialized loggers
steps_logger = logging.getLogger('steps')
http_logger = logging.getLogger('http')
validation_logger = logging.getLogger('validation')

Data Flow Architecture

Document Import Flow

sequenceDiagram
  participant F as Feature File
  participant S as Step Definition
  participant IC as ImportConfig
  participant C as Controller
  participant API as Backend API
  participant V as Validator
  F ->> S: When I POST document data
  S ->> IC: new ImportConfig(table_data)
  IC ->> IC: Parse & validate input
  IC ->> IC: Resolve enums
  IC ->> IC: Load user data
  S ->> IC: build_import_document(user)
  IC ->> IC: Create init section
  IC ->> IC: Create invoice items
  IC ->> IC: Add metadata
  IC ->> IC: Create organization data
  IC -->> S: document_body
  S ->> C: import_document(body)
  C ->> C: Add authentication
  C ->> API: POST /import/document/id
  API -->> C: 201 {id: "01118753"}
  C -->> S: response
  S ->> S: Store document_id in context
  F ->> S: When I GET html
  S ->> C: get_html_document(cid, doc_id)
  C ->> API: GET /document/{id}/html
  API -->> C: HTML response
  C -->> S: response
  S ->> V: parse HTML
  V ->> V: Extract values
  V -->> S: html_dict
  S ->> IC: AccountingDocumentBase(config)
  IC ->> IC: Calculate expected values
  IC -->> S: expected_data
  S ->> V: InvoiceModel.from_html(dict)
  V -->> S: received_data
  S ->> V: deepdiff_compare(expected, received)
  V -->> S: differences (or empty)
  S -->> F: Test result

Validation Flow

flowchart TD
  A[Response Received] --> B{Response Type?}
  B -->|HTML| C[BeautifulSoup Parse]
  B -->|JSON| D[JSON Parse]
  C --> E[Extract Values to Dict]
  D --> F[Load to Pydantic Model]
  E --> G[InvoiceModel.from_html]
  F --> H[JsonDocumentResponse model]
  G --> I[Received Model]
  H --> I
  J[ImportConfig] --> K[AccountingDocumentBase]
  K --> L[Calculate Expected Values]
  L --> M{Document Type?}
  M -->|HTML| N[InvoiceModel.from_document_base]
  M -->|JSON| O[JsonDocumentResponse.from_document_base]
  N --> P[Expected Model]
  O --> P
  I --> Q[model_dump]
  P --> R[model_dump]
  Q --> S[Round Floats]
  R --> S
  S --> T[DeepDiff Compare]
  T --> U{Differences?}
  U -->|Yes| V[Assert Fail with Details]
  U -->|No| W[Test Pass]
  style V fill: #ff6b6b
  style W fill: #51cf66

Model Layer Architecture

Pydantic Model Hierarchy

BaseModel (Pydantic)
├── MixedCaseModel (Custom base)
│   ├── ImportConfigInitModel
│   ├── Invoice
│   ├── JsonDocument
│   ├── JsonDocumentItem
│   ├── JsonDocumentResponse
│   ├── OandaJsonDocumentResponse
│   └── InvoiceModel
├── Item (Document item representation)
├── Organization (Organization data)
└── User (User data)

Model Design Principles

1. Separation by Purpose:

  • Import Models (document_import.py) - API request structures
  • Response Models (document_json.py, document_html.py) - API responses
  • Business Models (import_config.py, document_base.py) - Business logic
  • Enum Models (enums/) - Configuration and constants

2. Factory Methods:

class InvoiceModel(MixedCaseModel):
  """HTML document validation model"""

  @classmethod
  def from_html(cls, html_dict: dict, document_body: dict) -> Self:
    """Create from parsed HTML"""
    return cls(
      document_id=html_dict.get("document-id"),
      invoice_value=html_dict.get("invoice-number"),
      # ... extract and transform fields
    )

  @classmethod
  def from_document_base(cls, document_base: AccountingDocumentBase) -> Self:
    """Create from expected values"""
    payment_info = MetadataModel.build_expected_data(document_base)
    organization_info = OrganizationInfoModel.build_expected_data(document_base)
    invoice_dto = InvoiceModel.build_expected_data(document_base)

    return cls(**{
      **invoice_dto.model_dump(),
      **payment_info.model_dump(),
      **organization_info.model_dump()
    })

3. Validation at Boundaries:

class JsonDocumentResponse(MixedCaseModel):
  """Response model with validation"""

  id: str = Field(min_length=8, max_length=12)
  document: JsonDocument
  documentItems: list[JsonDocumentItem] = Field(min_length=1)

  model_config = ConfigDict(
    str_strip_whitespace=True,  # Clean strings
    validate_assignment=True,  # Validate on change
    extra='forbid'  # Reject unknown fields
  )

  @field_validator('id')
  @classmethod
  def validate_document_id(cls, v: str) -> str:
    """Custom validation for document ID format"""
    if not v.startswith(('01', '02', '90', '80', '70')):
      raise ValueError(f"Invalid document ID prefix: {v}")
    return v

Service Layer Design

HTTP Client Architecture

class Controller:
  """
  Main HTTP client for accounting service.

  Architecture:
  - Handles authentication via AuthController
  - Provides high-level API methods
  - Manages request/response lifecycle
  - Implements retry logic
  """

  def __init__(self, auth_controller: AuthController, base_url: str):
    self.auth = auth_controller
    self.base_url = base_url
    self.timeout = 30
    self.session = requests.Session()

  def __getitem__(self, key: tuple[str, str]) -> "Controller":
    """
    Indexer pattern for setting auth context.

    Usage:
        controller = context.controller["CLIENT_NBO", "service"]
        controller.import_document(body)
    """
    user_id, client_id = key
    self._current_user = user_id
    self._current_client = client_id
    return self

  def _get_headers(self) -> dict:
    """Build headers with authentication"""
    token = self.auth.get_token(self._current_user, self._current_client)
    return {
      "Authorization": f"Bearer {token.access_token}",
      "Content-Type": "application/json",
      "Accept": "application/json"
    }

  def _request(
    self,
    method: str,
    endpoint: str,
    **kwargs
  ) -> requests.Response:
    """
    Internal request method with error handling.

    Handles:
    - Timeout
    - Connection errors
    - HTTP errors
    - Logging
    """
    url = f"{self.base_url}{endpoint}"

    try:
      http_logger.debug(f"{method} {url}")
      response = self.session.request(
        method=method,
        url=url,
        headers=self._get_headers(),
        timeout=self.timeout,
        **kwargs
      )
      http_logger.debug(f"Response: {response.status_code}")
      return response

    except requests.Timeout:
      http_logger.error(f"Timeout after {self.timeout}s")
      raise
    except requests.ConnectionError as e:
      http_logger.error(f"Connection error: {e}")
      raise

  def import_document(self, body: dict) -> requests.Response:
    """Import document to accounting system"""
    return self._request(
      "POST",
      "/v1/service/import/document/id",
      json=body
    )

  def get_html_document(self, cid: int, document_id: str) -> requests.Response:
    """Retrieve HTML document"""
    return self._request(
      "GET",
      f"/v1/user/{cid}/document/{document_id}/html"
    )

Authentication Management

class AuthController:
  """
  Manages authentication tokens with caching.

  Features:
  - Token caching
  - Automatic refresh
  - Multiple client support
  """

  def __init__(self, users: dict, kc_base_url: str, default_client: str):
    self.users = users
    self._kc_base_url = kc_base_url
    self._default_client = default_client
    self._token_cache: dict[str, Token] = {}
    self._cache_lock = threading.Lock()

  def get_token(self, user_id: str, client_id: str) -> Token:
    """
    Get token with thread-safe caching.

    Flow:
    1. Check cache
    2. Validate expiry
    3. Return cached or fetch new
    """
    cache_key = f"{user_id}:{client_id}"

    with self._cache_lock:
      if cache_key in self._token_cache:
        token = self._token_cache[cache_key]
        if not token.is_expired():
          return token

      # Fetch new token
      token = self._fetch_token_from_keycloak(user_id, client_id)
      self._token_cache[cache_key] = token
      return token

  def _fetch_token_from_keycloak(
    self,
    user_id: str,
    client_id: str
  ) -> Token:
    """Fetch token from Keycloak"""
    user = self.users[user_id]

    response = requests.post(
      f"{self._kc_base_url}/realms/{self._realm}/protocol/openid-connect/token",
      data={
        "grant_type": "password",
        "client_id": client_id,
        "username": user.username,
        "password": user.password,
      }
    )

    if response.status_code != 200:
      raise AuthenticationError(f"Failed to get token: {response.text}")

    token_data = response.json()
    return Token(
      access_token=token_data["access_token"],
      expires_in=token_data["expires_in"],
      refresh_token=token_data.get("refresh_token"),
      fetched_at=datetime.now()
    )

Testing Strategy

Test Pyramid

        ┌─────────────┐
        │   E2E Tests │  (Feature files)
        │   (Slow)    │  Full flow validation
        └─────────────┘
        ┌────────────────┐
        │ Integration    │  (Step definitions + HTTP)
        │ Tests (Medium) │  API interaction
        └────────────────┘
        ┌────────────────────┐
        │   Unit Tests       │  (Helpers, Models)
        │   (Fast)           │  Business logic
        └────────────────────┘

Test Organization

By Type:

  • Regression Tests - Full feature coverage
  • Integration Tests - Multi-component scenarios
  • Unit Tests - Individual component testing

By Feature:

  • Document Import - Invoice/credit note import
  • Document Retrieval - HTML/JSON retrieval
  • Validation - Data validation
  • Access Control - GDPR and permissions
  • Multi-Platform - Helios, Xero, NetSuite, Oanda

Test Data Strategy

1. Fixture Files:

test_data/
├── payment_info_special_tip_a.json
├── payment_info_special_tip_b.json
├── payment_info_points.json
└── payment_info_challenge.json

2. Enum-Based Data:

class Company(Enum):
  """Company data as enum"""
  FTMO_EVAL_GLOBAL = CompanyField(
    item_id=5,
    full_name="FTMO Evaluation Global s.r.o.",
    address="Purkyňova 2121/3",
    # ... all company data
  )

3. Factory Functions:

def create_test_user(
  user_id: str,
  database: DatabaseSetting,
  country: Country = Country.CZ
) -> User:
  """Factory for creating test users"""
  return User(
    cid=generate_cid(),
    username=f"test_{user_id}",
    database=database,
    country=country,
  )

Extension Points

Adding New Platform

1. Define Platform:

# enums/enums_db_setting.py
class Platform(Enum):
  HELIOS = auto()
  XERO = auto()
  NETSUITE = auto()
  NEW_PLATFORM = auto()  # Add new platform

2. Implement Strategy:

# enums/enums_item_types.py
class ItemType(Enum):
  def item_id(self, db_setting: DatabaseSetting) -> str:
    match db_setting.platform:
      case Platform.HELIOS:
        return self.value.helios_type
      case Platform.XERO:
        return self.value.xero_type
      case Platform.NETSUITE:
        return self.value.code
      case Platform.NEW_PLATFORM:
        return self.value.new_platform_type  # Add mapping

3. Add Configuration:

# enums/enums_db_setting.py
class DatabaseSetting(Enum):
  NEW_DB = DatabaseConfig(
    db_name="NewPlatformDB",
    platform=Platform.NEW_PLATFORM,
    company=Company.NEW_COMPANY,
    # ... config
  )

Performance Considerations

1. Lazy Evaluation

Use @cached_property for expensive computations:

@cached_property
def total_vat_czk_money(self) -> Money:
  """Expensive calculation - cached"""
  if not self.show_czk:
    return Money(0, Currency.CZK)

  # Complex calculation
  czk_amount = self._calculate_vat_in_czk()
  return Money(czk_amount, Currency.CZK)

2. Connection Pooling

Use requests.Session() for connection reuse:

class Controller:
  def __init__(self, ...):
    self.session = requests.Session()  # Connection pool
    self.session.mount('https://', HTTPAdapter(
      pool_connections=10,
      pool_maxsize=20,
      max_retries=3
    ))

3. Parallel Test Execution

Use Behave's parallel execution:

# Run tests in parallel
behave --processes 4 --parallel-element scenario

Considerations:

  • Thread-safe context management
  • Isolated test data
  • No shared state between scenarios

Security Architecture

1. Credential Management

Never hardcode credentials:

# ❌ Wrong
password = "mypassword123"

# ✅ Correct
password = os.getenv("USER_PASSWORD")

# ✅ Better - use secrets manager
password = secrets_manager.get_secret("qa/user/password")

2. Token Security

Token handling:

class Token:
  """Secure token with expiry"""

  def __init__(self, access_token: str, expires_in: int, ...):
    self._access_token = access_token  # Private
    self._expires_at = datetime.now() + timedelta(seconds=expires_in)

  @property
  def access_token(self) -> str:
    """Read-only access to token"""
    if self.is_expired():
      raise TokenExpiredError("Token has expired")
    return self._access_token

  def is_expired(self) -> bool:
    """Check if token is expired (with buffer)"""
    buffer = timedelta(seconds=30)
    return datetime.now() >= (self._expires_at - buffer)

  def __repr__(self) -> str:
    """Don't expose token in repr"""
    return f"Token(expires_at={self._expires_at})"

3. Sensitive Data Handling

Masking in logs:

def log_request(url: str, headers: dict, body: dict):
  """Log request with sensitive data masked"""
  safe_headers = {
    k: "***REDACTED***" if k.lower() in ["authorization", "api-key"] else v
    for k, v in headers.items()
  }

  safe_body = mask_sensitive_fields(body, ["password", "secret", "token"])

  http_logger.debug(f"Request: {url}")
  http_logger.debug(f"Headers: {safe_headers}")
  http_logger.debug(f"Body: {json.dumps(safe_body)}")

Design Decisions

Why Static Exchange Rates?

Decision: Use static rates instead of live API

Rationale: - Test predictability (same input → same output) - No external dependencies - Fast test execution - Easy debugging with known rates

Trade-off: Rates don't match live markets, but tests verify logic not market data

Why Dual-Token Authentication?

Decision: Separate import_user and user contexts

Rationale: - Test service-level imports (CLIENT_NBO) - Test user-level access control - Mirrors production pattern - Enables cross-user tests

Implementation: Two controller instances per test

Why Pydantic Over Dataclasses?

Decision: Use Pydantic for all models

Rationale: - Runtime validation - Type safety - Serialization built-in - Better error messages - IDE autocompletion

Trade-off: Slightly more overhead, but worth it for safety

Why Behave Over pytest-bdd?

Decision: Use Behave for BDD

Rationale: - More mature Gherkin parser - Better feature file organization - Flexible hooks system - Comprehensive documentation

Trade-off: pytest-bdd better integrates with pytest, but Behave is sufficient