Skip to content

stovie93/integration-bench

Repository files navigation

Integration Test Bench

CI License: MIT Python 3.12

Four property-accounting vendor APIs — Entrata, Yardi Voyager, RealPage/OneSite, MRI — mocked faithfully enough to build real integrations against, because none of them hand out sandboxes. Push utility-bill spreadsheets through each vendor's actual wire dialect (JSON envelopes, SOAP, OData-ish), inspect the exact bytes, inject the failures each vendor is known for, and lock verified behavior in with captured-traffic regression fixtures.

Every implemented API method is transcribed from the vendor's official documentation — sources (archived official pages and vendor-published PDFs) and known gaps are tracked per vendor in docs/<vendor>/FIDELITY.md. Nothing is guessed; where the docs are silent (e.g. error envelopes) the gap is documented.

Two parts:

  • Mock server (app/) — impersonates vendor APIs with real state: charges you push land in a shared SQLite property database and can be queried back through any dialect (push via Yardi, read the same ledger via Entrata). All four vendors are implemented: Entrata, MRI, RealPage/OneSite, and Yardi Voyager (Billing & Payments).
  • Pusher (pusher/) — CLI that reads an Excel workbook, maps rows to API payloads via a YAML profile, validates them against the same schemas the server enforces, and pushes them (to the bench, or to a real endpoint).

The /demo page is the fastest tour — one click pushes the sample workbook through a vendor dialect and shows per-row results plus the exact wire traffic (/demo?auto=yardi runs one on load):

Sample workbook pushed through the Yardi SOAP dialect: per-row results and the exact request/response bodies

Quick start

python -m venv .venv
.\.venv\Scripts\pip install -r requirements.txt

# start the mock server
.\.venv\Scripts\python -m uvicorn app.main:create_app --factory --port 8000

# in another terminal: generate a sample workbook and push it
.\.venv\Scripts\python -m pusher sample bills.xlsx
.\.venv\Scripts\python -m pusher push bills.xlsx --profile pusher/profiles/entrata-utility-bills.r1.yaml
# or push the same workbook through the MRI or Yardi dialect instead:
.\.venv\Scripts\python -m pusher push bills.xlsx --profile pusher/profiles/mri-utility-bills.yaml
.\.venv\Scripts\python -m pusher push bills.xlsx --profile pusher/profiles/yardi-utility-bills.yaml

# see what landed
curl http://localhost:8000/admin/state/ar-transactions

Run tests: .\.venv\Scripts\python -m pytest

Config env vars: BENCH_DB (SQLite path, default bench.db), BENCH_USER/BENCH_PASS (Basic-auth creds, default testuser/testpass).

The Entrata mock

Where the real API is https://YOUR_DOMAIN.entrata.com/api/v1/{resource}, the mock is http://localhost:8000/entrata/api/v1/{resource} (the older documented /api/{resource} base also works). HTTP Basic auth; the request envelope follows the official template:

{
  "auth": {"type": "basic"},
  "requestId": 15,
  "method": {
    "name": "sendLeaseArTransactions",
    "version": "r1",
    "params": {
      "transaction": [{
        "transactionId": "UB-202606-3001-WATER",
        "leaseId": "3001",
        "arCodeId": "2001",
        "transactionAmount": "42.15",
        "transactionDate": "06/30/2026",
        "arPostMonth": "06/2026",
        "description": "Water service 06/01/2026-06/30/2026"
      }]
    }
  }
}

Implemented methods (all r1, matching the official docs; see GET / for the live list):

Resource Method Purpose
artransactions sendLeaseArTransactions push resident charges (per-transaction results in Transactions.Transaction[].@attributes)
artransactions getLeaseArTransactions full documented ledger structure: leases.lease.ledgers.ledger.transactions.transaction
arcodes getArCodes AR codes with debit/credit GL account ids
properties getProperties MITS-style PhysicalProperty.Property records with post months
vendors getVendors AP payees with locations
vendors sendInvoices AP invoice batches (ApBatch.ApHeaders.ApHeader[].ApDetails.ApDetail[])

Documented behavior you can test against (see FIDELITY.md for the full list):

  • Duplicate transactionId rejected ("Unique Transaction ID of client system") — exercise idempotency and retry logic.
  • Per-line failures — invalid lease, AR code, GL account, or payee location fails that line/header while the rest of the batch succeeds.
  • The XML-to-JSON traps: single-element collections arrive as an object, not a one-element array; empty descriptions arrive as []; write results hide in @attributes; some methods return everything as strings while others use native types; response envelopes differ per method. If your integration survives the mock, it's handling the real API's sharp edges.
  • Schema validation — params are validated against JSON Schemas transcribed from the official parameter tables; violations return error code 300 with the JSON path.

Versioning: schemas live in app/vendors/entrata/schemas/{method}.{version}.json. Adding a version (e.g. sendInvoices r2, which exists officially but wasn't in the archived docs) means dropping in a schema file and registering a handler — the dispatch machinery never changes.

The MRI mock

Where the real gateway is https://[MRIWebDomain]/mriapiservices/api.asp?$api=[APIName], the mock is http://localhost:8000/mri/mriapiservices/api.asp?$api=... (the /mriweb/mriapiservices base also works). Mechanics follow MRI's official Integrations Guide / Design Guide / MIX Quick Reference (docs/mri/): HTTP Basic auth with the 4-part clientID/database/username/partnerkey username (defaults are the docs' own example creds, P123456/MYDATABASE/WEBUSER/ZYXWVUTSRQ / p455w0rD; override via BENCH_MRI_USERNAME/BENCH_MRI_PASSWORD), GET/POST/PUT/MERGE plus X-HTTP-Method-Override, response shaping via $format=atom|json|xml|csv (AtomPub default, $format trumps headers), $metadata discovery, $top/$skip paging with NextPageLink, and save responses that echo every entry with per-entry Error/Message failures.

API Verbs Source
Vendors GET, POST, PUT/MERGE MIX Quick Reference samples
Units GET (paged, PROPERTYID=) Design Guide scenario
income-categories GET, POST, PUT/MERGE Integrations Guide examples
ResidentCharges GET, POST placeholder custom API — swap in your manifest's real definition (app/vendors/mri/apis.py)

MRI's model is that most APIs are defined per client via APIDesign, so ResidentCharges stands in for your company's actual charge API until its manifest definition is transcribed — see docs/mri/FIDELITY.md.

The RealPage / OneSite mock

SOAP dialect built from RealPage's official ODE Vendor Technical Specifications plus RealPage's own server-generated Unit.asmx templates (archived WSDL + operation pages; sources in docs/realpage/). Both URL generations are served:

  • ODE: POST /realpage/os-lr/webservices/availabilityandpricing/unit.asmx (case sensitive, small caps only — as documented) with the apikey header (default = the spec's example key, override BENCH_REALPAGE_APIKEY)
  • Legacy: POST /realpage/WebServices/CrossFire/AvailabilityAndPricing/Unit.asmx with UserName/Password inside the UserAuthInfo SOAP header (BENCH_REALPAGE_USER/BENCH_REALPAGE_PASS, default testuser/testpass)

GetUnitsByProperty is implemented against the official template — SOAP 1.1 and 1.2, UserAuthInfo SiteID routing (SiteID = property id in the seed data), WebServiceLogHeader response header, and the documented namespace trap where UnitObject's children carry xmlns="". No GL/charges service is faked — that surface was never public; docs/realpage/FIDELITY.md explains how to add it from your RealPage Exchange clone/partner spec.

The Yardi Voyager mock

POST /yardi/webservices/itfresidenttransactions20.asmx — the Billing & Payments interface. Provenance differs from the other vendors: Yardi's docs are licensed per client and were never public, so this adapter is cross-validated from three independent open-source production integrations that agree on the contract (see docs/yardi/FIDELITY.md — and verify against a real Voyager WSDL when one becomes available).

SOAP 1.1 with credentials in the operation body (UserName, Password, ServerName, Database, Platform, InterfaceEntity, InterfaceLicense — override via BENCH_YARDI_* env vars), _Login action names, and MITS ResidentTransactions documents:

Operation Purpose
ImportResidentTransactions_Login push charges (TransactionXml MITS document, one Charge/Detail per transaction)
GetResidentTransactions_Login read a property's ledger back as MITS
GetResidentTransactions_ByChargeDate_Login same, filtered by FromDate/ToDate

Auth failures reproduce Yardi's evidenced behavior: HTTP 200 with "Login failed." or "Invalid Interface Entity" in the body — the strings production integrations actually grep for. Other Itf interfaces return a not-implemented SOAP fault.

Admin plane (/admin, no auth — localhost/CI only)

Endpoint Purpose
PUT /admin/scenario/{vendor} inject failures: {"mode": ..., "delay_ms": 0, "fail_rate": 0.0}
GET /admin/requests?vendor=&limit= request log (every request/response recorded)
GET /admin/requests/{id} full request + response bodies
GET /admin/state/ar-transactions what actually landed (also /ap-invoices, /leases)
POST /admin/reset reseed database, clear log and scenarios

Generic modes (every vendor): normal, auth_fail (rejects even valid credentials), http_500, malformed (truncated body), timeout (holds the connection 30s so client timeouts fire). delay_ms adds latency in any mode; fail_rate (0–1) makes a random fraction of requests 500 — good for testing retry behavior.

Vendor-shaped modes (no-op on other vendors) fail the way that vendor actually fails:

Mode Vendor Behavior
api_error entrata HTTP 200 carrying the code-300 error envelope with the request's own requestId echoed back
invalid_entity yardi HTTP 200 whose body contains the evidenced literal Invalid Interface Entity
not_in_manifest mri the documented manifest-validation 500 (Failed to validate API...) — MRI's classic misconfiguration

Excel pusher

python -m pusher sample bills.xlsx                     # sample workbook matching seed data
python -m pusher validate bills.xlsx --profile <yaml>  # map + schema-check, no network
python -m pusher push bills.xlsx --profile <yaml> [--url ...] [--dry-run]

validate and push accept .xlsx or .csv (CSV needs a header row). Ready-made test files — clean, broken cells, unknown ids, duplicates, unicode, credits — live in examples/. The /demo page has the same flow in the browser: upload your own file, pick a vendor, and Validate (every bad cell reported) or Push (refused until the file maps cleanly).

A profile (pusher/profiles/*.yaml) declares the target vendor/method/version, the column-to-field mapping with types (str, int, money = 2-dp string, number, date with output format), optional row grouping, and where per-line results live in the response. A new spreadsheet layout or API version is a new profile, no code. --dry-run prints the built payloads; exit code is 0 only if every line succeeded, so it drops straight into CI.

--url/--user/--password point anywhere, so once a flow passes against the bench you can aim the same file at a real Entrata endpoint.

Capture & replay (golden fixtures)

The bench is only as faithful as its sources, and captured traffic from a real endpoint beats any documentation. The capture package closes that loop:

# recording reverse proxy: forwards to the real host, saves sanitized
# request/response pairs to fixtures/<vendor>/
python -m capture record --upstream https://client.entrata.com --vendor entrata
# point your integration (or the pusher) at http://localhost:8100

# replay fixtures against the bench and diff the responses
python -m capture replay [fixtures/entrata] [--url http://localhost:8000]

Credentials are scrubbed at record time (headers, Yardi/RealPage body auth fields), so fixtures are safe to commit; replay re-injects the bench's own defaults. The diff masks volatile values (server-assigned ids, UUIDs, timestamps) — everything else must match exactly. Fixtures marked golden: true are enforced on every test run by tests/test_fixtures.py; see fixtures/README.md for the golden-vs-reference distinction and the workflow when a live capture contradicts the bench (fix the handler, update FIDELITY.md).

Seed data

Three properties — 100001 Maple Court Apartments (leases 3001–3005), 100002 Birchwood Flats (leases 3006–3010), and 100003 Château Élan Lofts (leases 3011–3013), which is deliberately messy: accents in every string field, an apostrophe surname, and two leases on one unit (Past + Current) — the traps clean test data hides. Shared utility AR codes (2001 water, 2002 sewer, 2003 trash, 2004 electric, 2005 gas, 2006 late fee), GL accounts (income 42xx / expense 62xx, AR 1200), and AP payees 501/502 with locations 9501/9502. Edit app/store.py to change it; POST /admin/reset reseeds.

Adding a vendor or method

  1. Get the official documentation first — for Entrata, archived pages cover every method (see FIDELITY.md for the CDX query); for Yardi/RealPage/MRI the specs are behind partner portals, so use real captured traffic or partner docs. Do not guess payload shapes.
  2. Save the extracted spec under docs/<vendor>/.
  3. Transcribe the parameter table into a JSON Schema under app/vendors/<vendor>/schemas/ and register a handler with @register(vendor, resource, method, version, schema_path, envelope=...).
  4. For a new vendor, add a routes module that speaks its wire dialect (SOAP envelope parsing for Yardi/RealPage, OData-ish for MRI) and include it in app/main.py. The store, scenario injection, request log, and admin plane are shared.

Development

.\.venv\Scripts\python -m pytest tests/ -q   # full suite, incl. golden-fixture replay
.\.venv\Scripts\python -m ruff check .       # lint (CI enforces both)

Layout: app/ mock server (one module per vendor dialect under app/vendors/), pusher/ Excel/CSV CLI, capture/ record & replay, fixtures/ committed golden captures, docs/<vendor>/FIDELITY.md the per-vendor source-and-gaps ledger.

Status & disclaimer

This is an independent testing tool with no affiliation to, or endorsement by, Entrata, Yardi, RealPage, or MRI Software — all product names and trademarks belong to their owners. The mocks are transcribed from publicly available vendor documentation (sources per vendor in docs/<vendor>/FIDELITY.md) and contain no vendor code. Fidelity varies by vendor and every known gap or inference is documented — treat the bench as a development aid, and verify against a real environment before shipping an integration. All credentials in the repo are documentation examples or made-up test values.

MIT licensed — see LICENSE.

About

Mock property-accounting vendor APIs (Entrata, Yardi, RealPage/OneSite, MRI) + Excel utility-bill pusher, with wire-traffic capture & golden-fixture replay - test GL integrations without vendor sandboxes

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages