The suite already has Test Cases with several real brokers: the interoperability Test Cases in
IOP_TP, written for the 2026 test event. They cover Create Entity, Retrieve Entity and Query Entities
over four broker topologies. This page builds on them: what they do, what a run shows, what they need next, and a
long list of what else several real brokers could test, each item as a new IOP Test Case.
url in resources/variables.py. Every other broker is a stub
served by the HttpCtrl mock server on port 8086, told apart by a path prefix (/broker1,
/broker2).develop has 91 such Test Cases in TP/NGSI-LD/DistributedOperations: 21 for queries and
retrieval, 67 for provision and 3 for loop detection (a Via header that already names the
broker).IOP_TPTP: IOP_TP/NGSI-LD/Interoperability, 24 robot files,
each run twice, with the core @context and with the test suite's @context: 48 Test
Cases.b1_url to b5_url, passed on the command line. The brokers are
started outside the suite and start empty. The client always talks to b1.resources/IOPUtils/InteroperabilityUtils.resource: Compose IOP
Configuration takes a list of (entity id, registration file, mode, source broker, broker it is created
on) and creates the registrations; Delete Registrations And Entities removes them and deletes the
test's entities on every broker with local=true.data/entities/interoperability; 13 registrations in
data/csourceRegistrations/interoperability (inclusive, exclusive, redirect, auxiliary, some limited to
attributes or to one entity id).iop, the topology (cnf_01 to cnf_04), the registration modes
(additive-inclusive, additive-auxiliary, proxy-exclusive,
proxy-redirect) and the context.IOP_<operation>_<topology>_<variant>_<context>. Operation
001 = Create Entity, 002 = Retrieve Entity, 003 = Query Entities (GET and
POST).local=true, so no assertion
depends on one implementation's data.iop-tests-viewer
(tools/interoperability_visualizer), not yet in develop.| Topology | Brokers | Registrations | Test Cases |
|---|---|---|---|
cnf_01 | 3 | b1 → b2 inclusive, b1 → b3 exclusive | 001_01_01-02, 002_01_01-02, 003_01_01-02 |
cnf_02 | 4 | b1 → b2 inclusive, b1 → b3 redirect, b1 → b4 redirect | 001_02_01-02, 002_02_01-02, 003_02_01-02 |
cnf_03 | 4 | b1 → b2 auxiliary, b1 → b3 inclusive, b1 → b4 inclusive | 001_03_01-02, 002_03_01-02, 003_03_01-02 |
cnf_04 | 5 | b1 → b2 auxiliary, b1 → b3 inclusive; b2 → b4 redirect, b2 → b5 redirect; b3 → b5 exclusive. Two hops. | 001_04_01-02, 002_04_01-02, 003_04_01-02 |
Within a topology the registration files differ from one Test Case to the next (all attributes, a few attributes, one entity id), so the 24 files are 24 different setups.
Run on 6 November 2026 against five copies of one implementation, develop as of that day:
42 of 48 Test Cases pass, 21 of the 24 files. The three failing files:
IOP_003_02_02: the Test Case checks that b2's own OffStreetParking:1 equals b3's, and that
b3's own OffStreetParking:2 equals b4's, but its setup creates different versions there (the full entities in b2 and
b3, location and name only, with other locations, in b3 and b4). Each broker's own copy is to be compared with the
data created on it.
A Test Case issue, twice. b1's answer is right: the location with observedAt wins, as TS 104-175
clause 8 says.
IOP_002_04_01: b1 is expected to return OffStreetParking:1 with the location held in b5. The
only routes to b5 are b2's redirect for location, which b1 does not reach because its auxiliary
registration towards b2 is limited to availableSpotsNumber and totalSpotsNumber, and b3's
exclusive registration, which is exclusive-1: OffStreetParking:2, totalSpotsNumber.
Looks like a Test Case issue: 002_04_02 uses exclusive-3 (OffStreetParking:1,
location) in the same place and passes.
IOP_003_03_02: a query by type=OffStreetParking,Vehicle on b1 also returned
Vehicle:2, held in b4, whose registration covers OffStreetParking only.
That part of the Test Case is right and the fix belongs in the implementation: a forward never asks a source for
more than its registration covers. With it fixed, a second check fails: b1's Vehicle:1, from a query with
attrs=location, is compared with the full entity instead of the location-only one the Test Case
builds two lines before. A Test Case issue.
One documentation slip as well: IOP_001_03_01's description says the entity "shall not be present in
b3"; the Test Case checks, rightly, that it is not in b2.
Much of what an earlier version of this page asked for is already there: several broker URLs, a keyword that creates registrations between brokers, entities created on a chosen broker, a teardown over every broker, brokers started outside the suite, a tag. What is missing:
--variable list; a mixed run is
the same with a different list. This is task T4 of the TTF, Interoperability Test Automation via Scripted
Orchestration.operations other than
redirectionOps, management.timeout, tenant, contextSourceInfo,
jsonldContext, location, expiresAt. Compose IOP Configuration
takes them as they are.join. The OffStreetParking and Vehicle files stay.Via, the Entity Map location passed on, contextSourceInfo headers).003, one per operation, and topologies from
cnf_05 on, each described once (in the README or the viewer) and reused.The RV is the case this page started with. In IOP_TP terms it is two new configurations, cnf_05 and cnf_06, with the client talking to b1 as in every IOP Test Case.
A recreational vehicle is both a vehicle and a building. Its vehicle part lives in one broker and its building part
in another. A third broker, the one the client talks to, has a registration for each of them, both for the RV with
both of its types (an EntityInfo type can be an array, TS 104-175 § 5.2.6.6.2).
Broker b2: { "id": "urn:ngsi-ld:RV:1", "type": "Vehicle", "speed": { "type": "Property", "value": 80 } }
Broker b3: { "id": "urn:ngsi-ld:RV:1", "type": "Building", "floors": { "type": "Property", "value": 1 } }
Broker b1: inclusive registration urn:ngsi-ld:RV:1 / ["Vehicle", "Building"] -> b2
inclusive registration urn:ngsi-ld:RV:1 / ["Vehicle", "Building"] -> b3
NGSI-LD has no way to ask for only part of an entity. Whatever the request, when the RV is in the answer it is the
entire RV: "type": ["Vehicle", "Building"], with speed and
floors.
GET /entities/urn:ngsi-ld:RV:1 → the entire RV.GET /entities/urn:ngsi-ld:RV:1?type=Building → the entire RV.GET /entities?id=urn:ngsi-ld:RV:1 → the entire
RV.GET /entities?type=Building → the entire RV. It matches through its
building part, and the vehicle part comes with it.GET /entities?type=Vehicle,Building → the entire RV, once, not twice.entityMap=true), on the first page and on the next ones →
the entire RV. The map holds every registration for urn:ngsi-ld:RV:1, b2's as well as b3's.?type=Car → 404 for the retrieve, no RV in the query.Filters over both parts
GET /entities?type=Building&q=speed>50 → whether the RV matches depends on whether the
filter is applied before or after the parts are put together (splitEntities, TS 104-175
§ 4.3.3.3.3). Both settings get a Test Case.Three brokers, no stubs. The Test Case checks only what b1 answers, so b1, b2 and b3 can each be any implementation. Entities and registrations are new data files next to the OffStreetParking ones; the keywords are the existing ones.
The case the Entity Map exists for. b1's registration towards b2 is narrow, Vehicle only. The one towards b3 is wide, both types:
Broker b1: inclusive registration urn:ngsi-ld:RV:1 / "Vehicle" -> b2
inclusive registration urn:ngsi-ld:RV:1 / ["Vehicle", "Building"] -> b3
GET /entities?type=Vehicle&entityMap=true&splitEntities=true on b1:
urn:ngsi-ld:RV:1 goes into the Entity Map.type=Vehicle, so it is in the map entry for
urn:ngsi-ld:RV:1 too, although b3 holds the RV only as a Building.type=Vehicle, b3 would have nothing, since it holds the RV only as a Building."type": ["Vehicle", "Building"], speed and
floors.The same holds on every following page, and for the map read back through GET /entityMaps/{id}: the
entry for urn:ngsi-ld:RV:1 lists both registrations.
The rule behind it: the type chooses the registrations and the entities; the parts of a chosen entity are then fetched by its id. It follows from TS 104-175 § 9.6: the query is "a unitary operation, masking the fact" that the data is distributed, so b1 answers as one broker holding the whole RV would; and with split entities the map holds "candidate Entities", checked on the assembled entity, not on each part.
In the same setup, GET /entities?type=Building: b2's registration (Vehicle only) does not match, so
b2 is not asked and the answer is only the building part of the RV, "type": "Building" and
floors.
type=Building registers b2 for both types. The answer still holds everything
the matching sources have.Either way, a client cannot ask for only the part of type X: NGSI-LD has no such query. The Test Case for
type=Building is written once the group has chosen.
A long list, grouped. The number in brackets is how many brokers a Test Case needs; b1 is always the one the client talks to. IOP_TP marks what the existing Test Cases already cover, with the file. Every item is run with both contexts, as today.
attrs naming only a remote attribute: only that broker's part in the answer (3–5)
IOP_TP 002_02_02, 002_03_02, 002_04_02, 003_02_02, 003_03_02cnf_05?type=Vehicle with an Entity Map gives the entire RV, on every page, and the map lists both
registrations (3) — cnf_06idPattern plus type, instead of the RV's id (3)datasetIds: both instances in the answer (3)datasetId in two brokers: the merge rule of § 8.5 decides which one
is returned (3)expiresAt is in the past: discarded, the rest returned (3)pick and omit over a split entity: applied after merging (3)concise, keyValues and GeoJSON: the same merge in every format
(3)lang over a LanguageProperty held remotely (2)q over two attributes held in two brokers, splitEntities=true: matched after assembly
(3)splitEntities=false: the filter is forwarded and the entity does not match (3)georel with the GeoProperty in one broker and the queried attribute in another (3)scopeQ over a scope held remotely (2)idPattern matching entities in several brokers (4)count=true: each entity counted once, however many brokers hold a part of it (3)local=true: no broker is asked, only b1's own data (3)csf: only the matching registrations are used (4)location: a geo-query outside it does not reach that broker (3)idPattern only and on attribute names only (2 each)queryEntity among its operations: not asked (2)join/joinLevel with the linked entity in another broker (3)containedBy with linked entities over three brokers, and a cycle among them (4)Via: after three hops the header names all three brokers, each name checked against that broker's
/info/sourceIdentity (3, proxy)urn:ngsi-ld:null deletions (3)q over attributes in two brokers (3)timerel and lastN applied after merging (3)/types and /types?details=true: every broker's types, each once (3)/attributes, with and without details, over several brokers (3)/types/Vehicle with Vehicle entities in two brokers (3)contextSourceInfo: the headers it names reach b2 on every forwarded request (2, proxy)tenant: b1's tenant a reaches b2 as tenant b (2)jsonldContext: used on the forwarded request, the answer compacted back with
the client's context (2)management.timeout: a slow b2 is given up on, b3's data still returned (3)/info/sourceIdentity on every broker of the topology: all different (5)This is what IOP_TP was written for, and it needs no new Test Cases: the same Test Cases with each of
b1 to b5 a different implementation. With the start script of § 3 it becomes a matrix instead of one
result: for each Test Case, which implementation sat where. The ones worth running first:
Via identities of three implementations in one chain (3)IOP_001_03_01, so that a full run is a clean baseline. Ready: with it, 48 of 48 pass against five
copies of an implementation that respects the registrations.iop-tests-viewer branch
merged if its authors agree.cnf_05 and cnf_06, as the first new Test Cases, so the
group can judge the direction on one example.IOP_TP: a look at the Test Case issues of § 2.IOP_TP, with its names, tags and keywords.type=Building with a narrow registration towards b2.