ocpi-kit

Testkit

Sample objects, in-memory stores, and a conformant OCPI party you can point a partner's client at.

On this page

The testkit feature is the scaffolding every OCPI integration builds and nobody wants to maintain: conformant sample objects, stores that paginate the way the specification says, and a complete party you can run.

It is deliberately dependency-light — no HTTP mocking framework, no test runner — so it works from a unit test, an integration test, a fuzz target or a demo binary.

Sample objects

use ocpi_kit::testkit::sample;

let location = sample::location("LOC1")?;
let tariff = sample::tariff("T1", "0.25")?;

Every one validates and round-trips through JSON, asserted by a test — a sample that quietly stopped conforming would make every test built on it meaningless.

In-memory stores

use ocpi_kit::testkit::InMemoryLocations;

let store = InMemoryLocations::with_page_size(10);
store.put(sample::location("LOC1")?);
let page = store.page(&query, &base);   // Link, X-Total-Count and X-Limit already right

page() is the part worth having: offset, limit, date_from inclusive, date_to exclusive, X-Total-Count over the filtered set rather than the returned page, and a Link: rel="next" carrying the original filters forward. Each is a rule the conformance runner checks a real peer for.

MockPeer — the other side of the socket

use ocpi_kit::server::OcpiRouter;
use ocpi_kit::testkit::MockPeer;

let peer = MockPeer::cpo(base.clone()).seeded();
let app = peer.mount(OcpiRouter::new(VersionNumber::V2_3_0, base, peer.token_store())).build();

A complete OCPI party over those stores: both interfaces of Locations, Sessions, CDRs, Tariffs and Tokens, plus credentials. It is what ocpi serve-mock runs.

Writing this is otherwise the first day of every OCPI project, and the same day for everybody — a dozen handler traits over a HashMap, each an opportunity to get pagination or the ownership rule slightly wrong and then test against your own mistake.

Conformant, and checked rather than claimed. The test suite runs this crate's own conformance runner against it and requires a clean report: real-time authorization answers 2004 for an unknown token, a PUT under someone else's party is a 404, a PATCH without last_updated never reaches a handler, a re-POSTed CDR is refused, and offset/date_from are applied.

The stores are reachable through it, so a test can seed and inspect:

peer.locations.put(sample::location("LOC2")?);
assert_eq!(peer.cdrs.len(), 1);

MockPeer::msp(base) gives the other role. token_store() returns a store that accepts test_token("c") as the opposite party, so the router's ownership check behaves the way it would in a real deployment rather than waving everything through.

What it is not

Not a charge point. Commands, Charging Profiles and Payments are not mounted on purpose: a mock that answered ACCEPTED and never called the response_url back would teach a client the wrong lesson, and one that did call back would need a Charge Point to have an opinion. Version discovery advertises exactly what is mounted.

It speaks whichever version you publish

let app = peer.mount(OcpiRouter::new(VersionNumber::V2_2_1, base, peer.token_store())).build();

The handlers are written once, against the canonical 2.3.0 model; a 2.2.1 router answers 2.2.1 bytes, including a Tariff with no tax_included. See Versions and conversion.

Test parties and tokens

test_cpo() is NL/TNM, test_msp() is DE/ABC, test_hub() is NL/HUB, and test_token("c") is the literal test-token-c. Recognisable constants on purpose: one that leaks into a log or a fixture is obvious at a glance.

Improve this page on GitHub