LCOV - code coverage report
Current view: top level - antares-store/src - contract.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 98.3 % 1047 1029
Test Date: 2026-11-02 13:21:27 Functions: 42.0 % 112 47

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! The driver contract, as executable code.
       3              : //!
       4              : //! Every rule here is one a caller in `antares-api` relies on and no
       5              : //! backend may decide for itself. A driver that passes both functions can be
       6              : //! dropped into the broker; one that does not will break requests in ways
       7              : //! its own unit tests are free to miss, because a backend's tests assert
       8              : //! what that backend does, not what the seam promises.
       9              : //!
      10              : //! Both functions PANIC on the first violation, naming the rule. The caller
      11              : //! supplies two tenants that already exist (a backend may require a tenant
      12              : //! row before it accepts writes) and a prefix that makes the ids of this run
      13              : //! unique, so a shared database can host several runs at once.
      14              : //!
      15              : //! Both functions are `async`: a driver is awaited, never blocked on.
      16              : 
      17              : use crate::{CurrentStateDriver, CurrentStateDriverExt, Kind, TemporalDriver, TemporalDriverExt};
      18              : use antares_model::TenantId;
      19              : use serde_json::{json, Value};
      20              : 
      21          114 : fn doc(id: &str) -> Value {
      22          114 :     json!({"id": id, "type": "https://uri.etsi.org/ngsi-ld/default-context/T"})
      23          114 : }
      24              : 
      25              : /// Hold a current-state driver to the contract `antares-api` writes against.
      26              : ///
      27              : /// `a` and `b` are two existing tenants; `prefix` namespaces the ids this
      28              : /// run creates and deletes. Panics on the first rule broken.
      29            6 : pub async fn run_current_state_contract(
      30            6 :     d: &dyn CurrentStateDriver,
      31            6 :     a: &TenantId,
      32            6 :     b: &TenantId,
      33            6 :     prefix: &str,
      34            6 : ) {
      35            6 :     let e1 = format!("urn:ngsi-ld:{prefix}:1");
      36            6 :     let e2 = format!("urn:ngsi-ld:{prefix}:2");
      37            6 :     let gone = format!("urn:ngsi-ld:{prefix}:absent");
      38              : 
      39              :     // An absent row is absent, not an error and not an empty document.
      40            6 :     assert!(
      41            6 :         d.get(a, Kind::Entity, &gone).await.expect("get").is_none(),
      42              :         "get of an absent row must answer None"
      43              :     );
      44            6 :     assert!(
      45            6 :         !d.delete(a, Kind::Entity, &gone).await.expect("delete"),
      46              :         "delete of an absent row must answer false"
      47              :     );
      48              : 
      49              :     // ADR-0005 / ETSI 047_06: a mutate is a read-modify-write under the row
      50              :     // lock, and a missing row is None — NEVER an insert. A get+upsert
      51              :     // implementation lets a bookkeeping writeback racing a DELETE resurrect
      52              :     // the deleted row.
      53            6 :     let missed = d
      54            6 :         .mutate::<(), ()>(a, Kind::Entity, &gone, |_| Ok(()))
      55            6 :         .await
      56            6 :         .expect("mutate");
      57            6 :     assert!(missed.is_none(), "mutate of an absent row must answer None");
      58            6 :     assert!(
      59            6 :         d.get(a, Kind::Entity, &gone).await.expect("get").is_none(),
      60              :         "mutate of an absent row must not insert it (047_06)"
      61              :     );
      62              : 
      63              :     // create is create-if-absent: the second one is refused and changes
      64              :     // nothing.
      65            6 :     assert!(
      66            6 :         d.create(a, Kind::Entity, &e1, doc(&e1))
      67            6 :             .await
      68            6 :             .expect("create"),
      69              :         "the first create must report true"
      70              :     );
      71            6 :     assert!(
      72            6 :         !d.create(a, Kind::Entity, &e1, doc("urn:overwritten"))
      73            6 :             .await
      74            6 :             .expect("create"),
      75              :         "a create over an existing id must report false"
      76              :     );
      77            6 :     let row = d
      78            6 :         .get(a, Kind::Entity, &e1)
      79            6 :         .await
      80            6 :         .expect("get")
      81            6 :         .expect("row");
      82            6 :     assert_eq!(
      83            6 :         row["id"],
      84            6 :         e1.as_str(),
      85              :         "the refused create must not overwrite"
      86              :     );
      87              : 
      88              :     // A mutate that returns Ok commits; one that returns Err commits nothing.
      89            6 :     let applied = d
      90            6 :         .mutate::<(), ()>(a, Kind::Entity, &e1, |v| {
      91            6 :             v["marker"] = json!(1);
      92            6 :             Ok(())
      93            6 :         })
      94            6 :         .await
      95            6 :         .expect("mutate");
      96            6 :     assert_eq!(applied, Some(Ok(())), "a mutate over a present row applies");
      97            6 :     assert_eq!(
      98            6 :         d.get(a, Kind::Entity, &e1)
      99            6 :             .await
     100            6 :             .expect("get")
     101            6 :             .expect("row")["marker"],
     102            6 :         json!(1),
     103              :         "an accepted mutate must be visible to the next read"
     104              :     );
     105            6 :     let rejected = d
     106            6 :         .mutate::<(), &'static str>(a, Kind::Entity, &e1, |v| {
     107            6 :             v["marker"] = json!(2);
     108            6 :             Err("rejected")
     109            6 :         })
     110            6 :         .await
     111            6 :         .expect("mutate");
     112            6 :     assert_eq!(
     113              :         rejected,
     114              :         Some(Err("rejected")),
     115              :         "the closure's error crosses back"
     116              :     );
     117            6 :     assert_eq!(
     118            6 :         d.get(a, Kind::Entity, &e1)
     119            6 :             .await
     120            6 :             .expect("get")
     121            6 :             .expect("row")["marker"],
     122            6 :         json!(1),
     123              :         "a rejected mutate must commit nothing"
     124              :     );
     125              : 
     126              :     // A type list, where the driver keeps one, names the type of a live
     127              :     // Entity: `/types` answers from it without a fold.
     128            6 :     if let Some(names) = d.entity_type_names(a).await.expect("type list") {
     129            6 :         assert!(
     130            6 :             names
     131            6 :                 .iter()
     132            6 :                 .any(|t| t == "https://uri.etsi.org/ngsi-ld/default-context/T"),
     133              :             "the type list must name the type of a live Entity: {names:?}"
     134              :         );
     135            0 :     }
     136              : 
     137              :     // An attribute catalogue, where the driver keeps one, lists every
     138              :     // member a live Entity carries under each of its types, the
     139              :     // member a write added included.
     140            6 :     if let Some(pairs) = d.entity_attr_pairs(a).await.expect("attribute list") {
     141            6 :         let t = "https://uri.etsi.org/ngsi-ld/default-context/T";
     142            6 :         assert!(
     143           12 :             pairs.iter().any(|(ty, attr)| ty == t && attr == "marker"),
     144              :             "the attribute list must name a live Entity's member: {pairs:?}"
     145              :         );
     146            0 :     }
     147              : 
     148              :     // Tenant isolation, on every read and every write path.
     149            6 :     assert!(
     150            6 :         d.get(b, Kind::Entity, &e1).await.expect("get").is_none(),
     151              :         "another tenant must not read the row"
     152              :     );
     153            6 :     assert!(
     154            6 :         !d.delete(b, Kind::Entity, &e1).await.expect("delete"),
     155              :         "another tenant must not delete the row"
     156              :     );
     157            6 :     assert!(
     158            6 :         d.mutate::<(), ()>(b, Kind::Entity, &e1, |_| Ok(()))
     159            6 :             .await
     160            6 :             .expect("mutate")
     161            6 :             .is_none(),
     162              :         "another tenant must not mutate the row"
     163              :     );
     164            6 :     assert!(
     165            6 :         d.list(b, Kind::Entity)
     166            6 :             .await
     167            6 :             .expect("list")
     168            6 :             .iter()
     169            6 :             .all(|r| r["id"] != e1.as_str()),
     170              :         "another tenant must not list the row"
     171              :     );
     172              : 
     173              :     // Kinds are separate namespaces: the same id under two kinds is two rows.
     174            6 :     assert!(
     175            6 :         d.create(a, Kind::Subscription, &e1, doc(&e1))
     176            6 :             .await
     177            6 :             .expect("create"),
     178              :         "the same id under another kind is a different row"
     179              :     );
     180            6 :     assert!(d.delete(a, Kind::Subscription, &e1).await.expect("delete"));
     181            6 :     assert!(
     182            6 :         d.get(a, Kind::Entity, &e1).await.expect("get").is_some(),
     183              :         "deleting one kind must not touch another"
     184              :     );
     185              : 
     186              :     // upsert answers whether a document was ALREADY there, and batch_upsert
     187              :     // answers the opposite polarity — created-flags, which the batch path
     188              :     // needs to split 201 from 204 (5.6.8). Getting one of the two backwards
     189              :     // is invisible until a batch reports every create as an update.
     190            6 :     assert!(
     191            6 :         !d.upsert(a, Kind::Entity, &e2, doc(&e2))
     192            6 :             .await
     193            6 :             .expect("upsert"),
     194              :         "upsert that created the row reports false"
     195              :     );
     196            6 :     assert!(
     197            6 :         d.upsert(a, Kind::Entity, &e2, doc(&e2))
     198            6 :             .await
     199            6 :             .expect("upsert"),
     200              :         "upsert over an existing row reports true"
     201              :     );
     202            6 :     let e3 = format!("urn:ngsi-ld:{prefix}:3");
     203            6 :     let flags = d
     204            6 :         .batch_upsert(a, vec![(e3.clone(), doc(&e3)), (e2.clone(), doc(&e2))])
     205            6 :         .await
     206            6 :         .expect("batch_upsert");
     207            6 :     assert_eq!(
     208              :         flags,
     209            6 :         vec![true, false],
     210              :         "batch_upsert answers created-flags in input order, the opposite \
     211              :          polarity of upsert"
     212              :     );
     213            6 :     assert!(d.delete(a, Kind::Entity, &e3).await.expect("delete"));
     214            6 :     let ids = vec![e2.clone(), gone.clone(), e1.clone()];
     215            6 :     let batch = d
     216           12 :         .batch_mutate::<()>(a, &ids, |_, v| {
     217           12 :             v["batched"] = json!(true);
     218           12 :             Ok(())
     219           12 :         })
     220            6 :         .await
     221            6 :         .expect("batch_mutate");
     222            6 :     assert_eq!(batch.len(), ids.len(), "one result per input id");
     223            6 :     assert!(batch[0].is_some(), "results align with ids: {ids:?}");
     224            6 :     assert!(batch[1].is_none(), "an absent id answers None, in place");
     225            6 :     assert!(batch[2].is_some(), "results align with ids: {ids:?}");
     226              : 
     227              :     // A member whose closure refuses commits nothing while the others
     228              :     // commit: a batch write conditioned on each stored Entity
     229              :     // refuses member by member, and a refused member stays as it was.
     230            6 :     let pair = vec![e1.clone(), e2.clone()];
     231            6 :     let judged = d
     232           12 :         .batch_mutate::<&'static str>(a, &pair, |id, v| {
     233           12 :             v["judged"] = json!(true);
     234           12 :             if id == e2.as_str() {
     235            6 :                 Err("refused")
     236              :             } else {
     237            6 :                 Ok(())
     238              :             }
     239           12 :         })
     240            6 :         .await
     241            6 :         .expect("batch_mutate");
     242            6 :     assert_eq!(
     243              :         judged,
     244            6 :         vec![Some(Ok(())), Some(Err("refused"))],
     245              :         "each member's own answer, in place"
     246              :     );
     247            6 :     let accepted = d.get(a, Kind::Entity, &e1).await.expect("get");
     248            6 :     assert_eq!(
     249            6 :         accepted.expect("row")["judged"],
     250              :         json!(true),
     251              :         "an accepted member commits"
     252              :     );
     253            6 :     let refused = d.get(a, Kind::Entity, &e2).await.expect("get");
     254            6 :     assert!(
     255            6 :         refused.expect("row").get("judged").is_none(),
     256              :         "a refused member must commit nothing"
     257              :     );
     258              : 
     259              :     // A query may over-return (the caller re-checks whatever the backend
     260              :     // could not decide) but must never drop a matching row, and must never
     261              :     // cross a tenant.
     262            6 :     let ids_ref: Vec<&str> = vec![&e1, &e2];
     263            6 :     let mine = d
     264            6 :         .query_entities(
     265            6 :             a,
     266            6 :             &crate::filter::EntityFilter {
     267            6 :                 ids: Some(&ids_ref),
     268            6 :                 ..Default::default()
     269            6 :             },
     270            6 :         )
     271            6 :         .await
     272            6 :         .expect("query_entities");
     273           12 :     for want in [&e1, &e2] {
     274           12 :         assert!(
     275           18 :             mine.rows.iter().any(|r| r["id"] == want.as_str()),
     276              :             "query must not drop a matching row: {want}"
     277              :         );
     278              :     }
     279            6 :     let theirs = d
     280            6 :         .query_entities(
     281            6 :             b,
     282            6 :             &crate::filter::EntityFilter {
     283            6 :                 ids: Some(&ids_ref),
     284            6 :                 ..Default::default()
     285            6 :             },
     286            6 :         )
     287            6 :         .await
     288            6 :         .expect("query_entities");
     289            6 :     assert!(
     290            6 :         theirs.rows.is_empty(),
     291              :         "a query must never cross a tenant: {:?}",
     292              :         theirs.rows
     293              :     );
     294              : 
     295              :     // Paging pushdown is optional; claiming it and not doing it is not.
     296            6 :     let paged = d
     297            6 :         .query_entities(
     298            6 :             a,
     299            6 :             &crate::filter::EntityFilter {
     300            6 :                 ids: Some(&ids_ref),
     301            6 :                 page: Some(crate::filter::Page {
     302            6 :                     offset: 0,
     303            6 :                     limit: 1,
     304            6 :                     count: true,
     305            6 :                 }),
     306            6 :                 ..Default::default()
     307            6 :             },
     308            6 :         )
     309            6 :         .await
     310            6 :         .expect("query_entities");
     311            6 :     if paged.paged {
     312            6 :         assert!(
     313            6 :             paged.rows.len() <= 1,
     314              :             "a driver reporting paged=true has applied the LIMIT"
     315              :         );
     316            6 :         assert!(
     317            6 :             paged.decided,
     318              :             "paged implies decided: a LIMIT over an undecided set pages the wrong rows"
     319              :         );
     320            0 :     }
     321              :     // Keyset: `after_id` is optional too, but a driver that pages
     322              :     // must honour it, or a discovery walk reads its first page forever.
     323            6 :     let first = std::cmp::min(e1.as_str(), e2.as_str());
     324            6 :     let after = d
     325            6 :         .query_entities(
     326            6 :             a,
     327            6 :             &crate::filter::EntityFilter {
     328            6 :                 ids: Some(&ids_ref),
     329            6 :                 after_id: Some(first),
     330            6 :                 page: Some(crate::filter::Page {
     331            6 :                     offset: 0,
     332            6 :                     limit: 10,
     333            6 :                     count: false,
     334            6 :                 }),
     335            6 :                 ..Default::default()
     336            6 :             },
     337            6 :         )
     338            6 :         .await
     339            6 :         .expect("query_entities");
     340            6 :     if after.paged {
     341            6 :         assert!(
     342            6 :             after.rows.iter().all(|r| r["id"].as_str() > Some(first)),
     343              :             "a paging driver returns only ids after `after_id`: {:?}",
     344              :             after.rows
     345              :         );
     346            6 :         assert_eq!(after.rows.len(), 1, "the one id after {first} is kept");
     347            0 :     }
     348              : 
     349              :     // `subscription_tenants` is the iteration domain the interval sweep and
     350              :     // both mirror hydrations walk. A backend may return MORE tenants than
     351              :     // hold a subscription — every caller lists per tenant afterwards and an
     352              :     // empty list costs nothing. It may never return FEWER: a tenant missing
     353              :     // here is a tenant whose periodic notifications never fire and whose
     354              :     // subscriptions never reach the mirror, with no error anywhere.
     355            6 :     let sub = format!("urn:ngsi-ld:Subscription:{prefix}:1");
     356            6 :     let sub_doc = json!({
     357            6 :         "id": &sub,
     358            6 :         "type": "Subscription",
     359            6 :         "status": "active",
     360            6 :         "notification": {"endpoint": {"uri": "http://sink.invalid/n"}},
     361              :     });
     362            6 :     d.upsert(a, Kind::Subscription, &sub, sub_doc)
     363            6 :         .await
     364            6 :         .expect("upsert subscription");
     365            6 :     let domain = d
     366            6 :         .subscription_tenants()
     367            6 :         .await
     368            6 :         .expect("subscription_tenants");
     369            6 :     assert!(
     370           26 :         domain.iter().any(|t| t == a.as_str()),
     371              :         "a tenant holding a subscription must appear in subscription_tenants: {domain:?}"
     372              :     );
     373              :     // A REGISTRATION puts a tenant in the domain too. One of the hydrations
     374              :     // that walks this domain fills the registration mirror, and the
     375              :     // federation path reads that mirror alone once it is installed — so a
     376              :     // tenant holding registrations and no subscription is a tenant that
     377              :     // silently forwards to no Context Source.
     378            6 :     let reg_only = TenantId::new(&format!("{prefix}regonly")).expect("tenant");
     379            6 :     let reg = format!("urn:ngsi-ld:ContextSourceRegistration:{prefix}:1");
     380            6 :     d.upsert(
     381            6 :         &reg_only,
     382            6 :         Kind::Registration,
     383            6 :         &reg,
     384            6 :         json!({
     385            6 :             "id": &reg,
     386            6 :             "type": "ContextSourceRegistration",
     387            6 :             "endpoint": "http://cs.invalid/ngsi-ld/v1",
     388            6 :             "information": [{"entities": [{"type": "Vehicle"}]}],
     389            6 :         }),
     390            6 :     )
     391            6 :     .await
     392            6 :     .expect("upsert registration");
     393            6 :     let domain = d
     394            6 :         .subscription_tenants()
     395            6 :         .await
     396            6 :         .expect("subscription_tenants");
     397            6 :     assert!(
     398           52 :         domain.iter().any(|t| t == reg_only.as_str()),
     399              :         "a tenant holding only a registration must appear in the mirror-hydration \
     400              :          domain, or its registration mirror hydrates empty: {domain:?}"
     401              :     );
     402              : 
     403              :     // Table 5.2.9-2 forward bookkeeping, pinned here for the same reason as
     404              :     // 5.2.14.2's: a backend may write it as one statement instead of a
     405              :     // read-modify-write, and the result must not depend on which it chose.
     406            6 :     let f1 = "2020-02-01T00:00:00.000Z";
     407            6 :     let f2 = "2020-02-02T00:00:00.000Z";
     408            6 :     let ok1 = d
     409            6 :         .record_forward(&reg_only, &reg, f1, true)
     410            6 :         .await
     411            6 :         .expect("record_forward")
     412            6 :         .expect("the registration is there");
     413            6 :     assert_eq!(
     414            6 :         ok1["timesSent"], 1,
     415              :         "the first forward moves timesSent to 1"
     416              :     );
     417            6 :     assert_eq!(ok1["lastSuccess"], f1, "lastSuccess takes the stamp");
     418            6 :     assert_eq!(ok1["status"], "ok", "a 2xx leaves the registration ok");
     419            6 :     assert!(
     420            6 :         ok1.get("timesFailed").is_none() && ok1.get("lastFailure").is_none(),
     421              :         "a registration that has only succeeded carries no failure members: {ok1}"
     422              :     );
     423              : 
     424            6 :     let bad = d
     425            6 :         .record_forward(&reg_only, &reg, f2, false)
     426            6 :         .await
     427            6 :         .expect("record_forward")
     428            6 :         .expect("the registration is still there");
     429            6 :     assert_eq!(
     430            6 :         bad["timesSent"], 2,
     431              :         "timesSent counts the failed attempt too (Table 5.2.9-2)"
     432              :     );
     433            6 :     assert_eq!(bad["timesFailed"], 1, "the failure moves timesFailed");
     434            6 :     assert_eq!(bad["lastFailure"], f2, "lastFailure takes the stamp");
     435            6 :     assert_eq!(bad["status"], "failed", "status names the LAST attempt");
     436            6 :     assert_eq!(
     437            6 :         bad["lastSuccess"], f1,
     438              :         "a failure never rewinds the last success"
     439              :     );
     440              : 
     441              :     // A registration deleted while its forward was in flight has no row to
     442              :     // book against, and the writeback must not resurrect it.
     443            6 :     let vanished = format!("urn:ngsi-ld:ContextSourceRegistration:{prefix}:gone");
     444            6 :     assert!(
     445            6 :         d.record_forward(&reg_only, &vanished, f1, true)
     446            6 :             .await
     447            6 :             .expect("record_forward")
     448            6 :             .is_none(),
     449              :         "an absent registration books nothing"
     450              :     );
     451            6 :     assert!(
     452            6 :         d.get(&reg_only, Kind::Registration, &vanished)
     453            6 :             .await
     454            6 :             .expect("get")
     455            6 :             .is_none(),
     456              :         "a bookkeeping writeback must never insert the row it missed"
     457              :     );
     458              : 
     459            6 :     d.delete(&reg_only, Kind::Registration, &reg)
     460            6 :         .await
     461            6 :         .expect("delete");
     462              :     // `list_page` is how the readers that must see EVERY document read —
     463              :     // the mirror seed above all — so a backend may not refuse it for volume
     464              :     // the way `list` may (5.5.6 licenses TooManyResults for a query
     465              :     // operation, which an internal bootstrap is not). Every arm must agree
     466              :     // on the walk, including the one that takes the trait default: ids
     467              :     // strictly greater than `after`, id-ordered, at most `limit`, a short
     468              :     // page means the end, and every stored document served exactly once.
     469            6 :     let mut ids: Vec<String> = (0..7)
     470           42 :         .map(|i| format!("urn:ngsi-ld:Subscription:{prefix}:page:{i}"))
     471            6 :         .collect();
     472           42 :     for id in &ids {
     473           42 :         d.upsert(
     474           42 :             a,
     475           42 :             Kind::Subscription,
     476           42 :             id,
     477           42 :             json!({"id": id, "type": "Subscription"}),
     478           42 :         )
     479           42 :         .await
     480           42 :         .expect("upsert page subscription");
     481              :     }
     482            6 :     ids.push(sub.clone());
     483            6 :     ids.sort();
     484              : 
     485            6 :     let mut walked: Vec<String> = Vec::new();
     486            6 :     let mut after: Option<String> = None;
     487              :     loop {
     488           18 :         let page = d
     489           18 :             .list_page(a, Kind::Subscription, after.as_deref(), 3)
     490           18 :             .await
     491           18 :             .expect("list_page");
     492           18 :         assert!(page.len() <= 3, "a page may not exceed its limit: {page:?}");
     493           18 :         let short = page.len() < 3;
     494           48 :         for doc in &page {
     495           48 :             let id = doc["id"].as_str().expect("a stored doc keeps its id");
     496           48 :             assert!(
     497           48 :                 after.as_deref().is_none_or(|prev| id > prev),
     498              :                 "`after` is exclusive and the walk ascends: {id} after {after:?}"
     499              :             );
     500           48 :             after = Some(id.to_owned());
     501           48 :             walked.push(id.to_owned());
     502              :         }
     503           18 :         if short {
     504            6 :             break;
     505           12 :         }
     506              :     }
     507            6 :     assert_eq!(
     508              :         walked, ids,
     509              :         "the walk must serve every subscription of the tenant exactly once, in id order"
     510              :     );
     511              :     // The other tenant's documents are not on this walk. 4.14: operations
     512              :     // "only apply to the information of the specified `Tenant` in isolation".
     513            6 :     assert!(
     514            6 :         d.list_page(b, Kind::Subscription, None, 100)
     515            6 :             .await
     516            6 :             .expect("list_page")
     517            6 :             .is_empty(),
     518              :         "a tenant with no subscriptions pages empty, whatever another tenant holds"
     519              :     );
     520              :     // Entities page like every other kind. 5.9.2.4's registration-vs-entity
     521              :     // conflict check reads them this way — it must see every Entity of the
     522              :     // tenant and has no TooManyResults to raise — so a backend may not refuse
     523              :     // this walk for volume either, and may not build the page by
     524              :     // materializing the tenant first.
     525              :     //
     526              :     // 4.22 applies INSIDE the page: an expired Entity is not there to be
     527              :     // served, and dropping one after the limit was applied would hand back a
     528              :     // short page, which every walker reads as the end of the tenant.
     529            6 :     let stem = format!("urn:ngsi-ld:{prefix}:page:");
     530           24 :     let live: Vec<String> = (0..4).map(|i| format!("{stem}live{i}")).collect();
     531              :     // sorts between live1 and live2, so it falls inside the first page below
     532            6 :     let expired = format!("{stem}live1x");
     533           24 :     for id in &live {
     534           24 :         d.upsert(a, Kind::Entity, id, doc(id))
     535           24 :             .await
     536           24 :             .expect("upsert page entity");
     537              :     }
     538            6 :     let mut gone_doc = doc(&expired);
     539            6 :     gone_doc["expiresAt"] = json!("2000-01-01T00:00:00.000Z");
     540            6 :     d.upsert(a, Kind::Entity, &expired, gone_doc)
     541            6 :         .await
     542            6 :         .expect("upsert expired entity");
     543              : 
     544            6 :     let page = d
     545            6 :         .list_page(a, Kind::Entity, Some(&stem), 3)
     546            6 :         .await
     547            6 :         .expect("list_page entities");
     548           18 :     let served: Vec<&str> = page.iter().filter_map(|d| d["id"].as_str()).collect();
     549            6 :     assert_eq!(
     550              :         served,
     551            6 :         [live[0].as_str(), live[1].as_str(), live[2].as_str()],
     552              :         "an expired entity may neither be served nor consume a slot of the page"
     553              :     );
     554              : 
     555            6 :     let mut walked: Vec<String> = Vec::new();
     556            6 :     let mut after = stem.clone();
     557              :     loop {
     558           18 :         let page = d
     559           18 :             .list_page(a, Kind::Entity, Some(&after), 2)
     560           18 :             .await
     561           18 :             .expect("list_page entities");
     562           18 :         let short = page.len() < 2;
     563           18 :         let mut moved = false;
     564           24 :         for doc in &page {
     565           24 :             let id = doc["id"].as_str().expect("a stored entity keeps its id");
     566           24 :             assert!(
     567           24 :                 id > after.as_str(),
     568              :                 "`after` is exclusive: {id} after {after}"
     569              :             );
     570           24 :             after = id.to_owned();
     571           24 :             moved = true;
     572           24 :             if id.starts_with(stem.as_str()) {
     573           24 :                 walked.push(id.to_owned());
     574           24 :             }
     575              :         }
     576           18 :         if short || !moved || !after.starts_with(stem.as_str()) {
     577            6 :             break;
     578           12 :         }
     579              :     }
     580            6 :     assert_eq!(
     581              :         walked, live,
     582              :         "the walk must serve every live entity of the tenant exactly once, in id \
     583              :          order, and never the expired one"
     584              :     );
     585           30 :     for id in live.iter().chain(std::iter::once(&expired)) {
     586           30 :         d.delete(a, Kind::Entity, id)
     587           30 :             .await
     588           30 :             .expect("delete page entity");
     589              :     }
     590              : 
     591              :     // 4.22 at the read boundary, the part a backend must not decide for
     592              :     // itself. The stamp marks "a certain Entity, Property or Relationship"
     593              :     // invalid; it is not a store-wide rule that hides any document carrying
     594              :     // one, and it reaches every Attribute of an Entity, sub-Attributes
     595              :     // included.
     596            6 :     let exp_e = format!("urn:ngsi-ld:{prefix}:expiry:entity");
     597            6 :     let mut expired_entity = doc(&exp_e);
     598            6 :     expired_entity["expiresAt"] = json!("2000-01-01T00:00:00.000Z");
     599            6 :     d.upsert(a, Kind::Entity, &exp_e, expired_entity)
     600            6 :         .await
     601            6 :         .expect("upsert expired entity");
     602            6 :     assert!(
     603            6 :         d.get(a, Kind::Entity, &exp_e).await.expect("get").is_none(),
     604              :         "an Entity past its expiresAt reads absent, not just out of a page"
     605              :     );
     606            6 :     d.delete(a, Kind::Entity, &exp_e).await.expect("delete");
     607              : 
     608              :     // An Attribute whose every instance expired leaves; the Entity carrying
     609              :     // it does not, and neither does a live sibling.
     610            6 :     let exp_a = format!("urn:ngsi-ld:{prefix}:expiry:attr");
     611           78 :     let iri = |n: &str| format!("https://uri.etsi.org/ngsi-ld/default-context/{n}");
     612            6 :     let mut with_attrs = doc(&exp_a);
     613            6 :     with_attrs[iri("gone")] = json!([{"value": 1, "expiresAt": "2000-01-01T00:00:00.000Z"}]);
     614            6 :     with_attrs[iri("kept")] = json!([{
     615            6 :         "value": 2,
     616            6 :         iri("subgone"): [{"value": 3, "expiresAt": "2000-01-01T00:00:00.000Z"}],
     617            6 :         iri("subkept"): [{"value": 4}],
     618              :     }]);
     619              :     // 4.6.3 leaves the seconds-fraction separator open, and ',' (0x2C) sorts
     620              :     // before both '.' and 'Z', so the two stamps have to be read as instants:
     621              :     // a backend that compares them as bytes serves this live Attribute as
     622              :     // expired. Every backend answers the same here or it is not the same
     623              :     // store.
     624            6 :     with_attrs[iri("commakept")] = json!([{"value": 5, "expiresAt": "2999-01-01T00:00:00,500Z"}]);
     625            6 :     d.upsert(a, Kind::Entity, &exp_a, with_attrs)
     626            6 :         .await
     627            6 :         .expect("upsert entity with an expired attribute");
     628            6 :     let served = d
     629            6 :         .get(a, Kind::Entity, &exp_a)
     630            6 :         .await
     631            6 :         .expect("get")
     632            6 :         .expect("an Entity outlives the expiry of one of its Attributes");
     633            6 :     assert!(
     634            6 :         served.get(iri("gone")).is_none(),
     635              :         "an Attribute whose only instance expired is not served"
     636              :     );
     637            6 :     assert_eq!(
     638            6 :         served[iri("kept")][0]["value"],
     639            6 :         json!(2),
     640              :         "a live Attribute survives its sibling's expiry"
     641              :     );
     642            6 :     assert!(
     643            6 :         served[iri("kept")][0].get(iri("subgone")).is_none(),
     644              :         "a sub-Attribute is a Property or Relationship too: past its stamp it \
     645              :          is not served"
     646              :     );
     647            6 :     assert_eq!(
     648            6 :         served[iri("kept")][0][iri("subkept")][0]["value"],
     649            6 :         json!(4),
     650              :         "a live sub-Attribute survives its sibling's expiry"
     651              :     );
     652            6 :     assert_eq!(
     653            6 :         served[iri("commakept")][0]["value"],
     654            6 :         json!(5),
     655              :         "a comma seconds-fraction is an instant, not a byte string"
     656              :     );
     657              :     // attrs= asks for an Entity that holds one of them NOW: an Attribute
     658              :     // whose every instance expired is not held (4.22), so an answer that
     659              :     // claims to be decided must not name the Entity for it. An undecided
     660              :     // one is re-judged by the caller over the served (stripped) rows.
     661            6 :     let only_gone = [iri("gone")];
     662            6 :     let by_attr = d
     663            6 :         .query_entities(
     664            6 :             a,
     665            6 :             &crate::filter::EntityFilter {
     666            6 :                 attrs: Some(&only_gone),
     667            6 :                 ..Default::default()
     668            6 :             },
     669            6 :         )
     670            6 :         .await
     671            6 :         .expect("query_entities");
     672            6 :     assert!(
     673            6 :         !by_attr.decided || by_attr.rows.iter().all(|r| r["id"] != exp_a.as_str()),
     674              :         "attrs= over an Attribute whose only instance expired: a decided \
     675              :          answer named the Entity"
     676              :     );
     677            6 :     assert!(
     678            6 :         by_attr
     679            6 :             .rows
     680            6 :             .iter()
     681            6 :             .filter(|r| r["id"] == exp_a.as_str())
     682            6 :             .all(|r| r.get(iri("gone")).is_none()),
     683              :         "a query served an expired Attribute"
     684              :     );
     685            6 :     d.delete(a, Kind::Entity, &exp_a).await.expect("delete");
     686              : 
     687              :     // batch_upsert takes each id once: the API splits repeats into rounds
     688              :     // (5.6.8 / 5.6.10), and what a backend does with a repeat inside one
     689              :     // call is its own guess (Postgres refuses an ON CONFLICT row affected
     690              :     // twice). A repeat is refused on every arm, and nothing is written.
     691            6 :     let dup = format!("urn:ngsi-ld:{prefix}:dup");
     692            6 :     assert!(
     693            6 :         d.batch_upsert(a, vec![(dup.clone(), doc(&dup)), (dup.clone(), doc(&dup))])
     694            6 :             .await
     695            6 :             .is_err(),
     696              :         "batch_upsert with an id twice must be refused"
     697              :     );
     698            6 :     assert!(
     699            6 :         d.get(a, Kind::Entity, &dup).await.expect("get").is_none(),
     700              :         "a refused batch_upsert wrote nothing"
     701              :     );
     702              : 
     703              :     // 5.8.6: an expired SUBSCRIPTION is not deleted and stays retrievable —
     704              :     // the API turns the stamp into status "expired" and keeps it updatable.
     705              :     // A backend that hid every document with a past expiresAt would lose it.
     706            6 :     let exp_s = format!("urn:ngsi-ld:{prefix}:expiry:sub");
     707            6 :     d.upsert(
     708            6 :         a,
     709            6 :         Kind::Subscription,
     710            6 :         &exp_s,
     711            6 :         json!({
     712            6 :             "id": exp_s,
     713            6 :             "type": "Subscription",
     714            6 :             "expiresAt": "2000-01-01T00:00:00.000Z",
     715            6 :         }),
     716            6 :     )
     717            6 :     .await
     718            6 :     .expect("upsert expired subscription");
     719            6 :     assert!(
     720            6 :         d.get(a, Kind::Subscription, &exp_s)
     721            6 :             .await
     722            6 :             .expect("get")
     723            6 :             .is_some(),
     724              :         "an expired Subscription stays retrievable (5.8.6)"
     725              :     );
     726            6 :     d.delete(a, Kind::Subscription, &exp_s)
     727            6 :         .await
     728            6 :         .expect("delete");
     729              : 
     730              :     // `delete_entity_if` decides and deletes under one lock, and it decides
     731              :     // on the STORED document. 5.6.6.4: an Entity the caller's selector
     732              :     // excludes "is not known" for the operation, which is the same answer an
     733              :     // absent Entity gets — so a refusal and a miss are both `false`, and a
     734              :     // refusal must leave the document exactly where it was.
     735            6 :     let cond = format!("urn:ngsi-ld:{prefix}:cond");
     736            6 :     d.upsert(a, Kind::Entity, &cond, doc(&cond))
     737            6 :         .await
     738            6 :         .expect("upsert conditional-delete entity");
     739            6 :     assert!(
     740            6 :         !d.delete_entity_if(a, &cond, &|_| false)
     741            6 :             .await
     742            6 :             .expect("delete_entity_if"),
     743              :         "a refused predicate reports no deletion"
     744              :     );
     745            6 :     assert!(
     746            6 :         d.get(a, Kind::Entity, &cond).await.expect("get").is_some(),
     747              :         "a refused delete leaves the document in place"
     748              :     );
     749              :     // 4.14: the predicate never even runs for another tenant's document.
     750            6 :     assert!(
     751            6 :         !d.delete_entity_if(b, &cond, &|_| panic!(
     752              :             "another tenant's document reached the predicate"
     753              :         ))
     754            6 :         .await
     755            6 :         .expect("delete_entity_if"),
     756              :         "an Entity of another tenant is absent here"
     757              :     );
     758            6 :     assert!(
     759            6 :         d.get(a, Kind::Entity, &cond).await.expect("get").is_some(),
     760              :         "a delete addressed to another tenant may not touch this one"
     761              :     );
     762            6 :     assert!(
     763            6 :         d.delete_entity_if(a, &cond, &|v| v["id"] == cond.as_str())
     764            6 :             .await
     765            6 :             .expect("delete_entity_if"),
     766              :         "the predicate is handed the stored document, not the id"
     767              :     );
     768            6 :     assert!(
     769            6 :         d.get(a, Kind::Entity, &cond).await.expect("get").is_none(),
     770              :         "an accepted predicate deletes"
     771              :     );
     772            6 :     assert!(
     773            6 :         !d.delete_entity_if(a, &cond, &|_| true)
     774            6 :             .await
     775            6 :             .expect("delete_entity_if"),
     776              :         "an absent Entity is not deleted, whatever the predicate answers"
     777              :     );
     778              : 
     779              :     // `list_slice` is the window a 5.5.9.2 limit/offset listing serves from,
     780              :     // and it must agree with the walk above on both the order and the set:
     781              :     // the same ids, the same order, and a total that counts the whole match
     782              :     // set rather than the page. A backend may not refuse it for volume — the
     783              :     // window bounds the result by construction.
     784            6 :     let (page, total) = d
     785            6 :         .list_slice(a, Kind::Subscription, 0, 3)
     786            6 :         .await
     787            6 :         .expect("list_slice");
     788            6 :     assert_eq!(total, ids.len(), "the total counts the set, not the page");
     789            6 :     assert_eq!(page.len(), 3, "limit is a maximum, and 3 were available");
     790           18 :     for (i, doc) in page.iter().enumerate() {
     791           18 :         assert_eq!(
     792           18 :             doc["id"].as_str(),
     793           18 :             Some(ids[i].as_str()),
     794              :             "the window is the id-ordered prefix: {page:?}"
     795              :         );
     796              :     }
     797              :     // Every single-element window lands on the element the walk has there.
     798           48 :     for (i, want) in ids.iter().enumerate() {
     799           48 :         let (page, _) = d
     800           48 :             .list_slice(a, Kind::Subscription, i, 1)
     801           48 :             .await
     802           48 :             .expect("list_slice");
     803           48 :         assert_eq!(
     804           48 :             page.first().and_then(|d| d["id"].as_str()),
     805           48 :             Some(want.as_str()),
     806              :             "offset {i} served the wrong element: {page:?}"
     807              :         );
     808              :     }
     809              :     // limit 0 is legal (6.3.10, with count): the count without the page.
     810            6 :     let (page, total) = d
     811            6 :         .list_slice(a, Kind::Subscription, 0, 0)
     812            6 :         .await
     813            6 :         .expect("list_slice");
     814            6 :     assert!(page.is_empty(), "limit 0 returns no elements: {page:?}");
     815            6 :     assert_eq!(total, ids.len(), "limit 0 still counts the whole set");
     816              :     // An offset past the end is an empty page, not an error, and the total
     817              :     // is unchanged by where the client asked to start.
     818            6 :     let (page, total) = d
     819            6 :         .list_slice(a, Kind::Subscription, ids.len() + 10, 5)
     820            6 :         .await
     821            6 :         .expect("list_slice");
     822            6 :     assert!(page.is_empty(), "past the end is empty: {page:?}");
     823            6 :     assert_eq!(total, ids.len(), "an offset does not change the match set");
     824              :     // 4.14 again: the window is inside one tenant.
     825            6 :     let (page, total) = d
     826            6 :         .list_slice(b, Kind::Subscription, 0, 100)
     827            6 :         .await
     828            6 :         .expect("slice");
     829            6 :     assert!(
     830            6 :         page.is_empty() && total == 0,
     831              :         "another tenant's rows reached this window: {page:?} / {total}"
     832              :     );
     833              : 
     834           48 :     for id in ids.iter().filter(|i| *i != &sub) {
     835           42 :         d.delete(a, Kind::Subscription, id).await.expect("delete");
     836              :     }
     837              : 
     838              :     // 5.2.14.2 delivery bookkeeping. A backend may write this as one
     839              :     // statement instead of a read-modify-write; the result must not depend on
     840              :     // which it chose, so the rule is pinned here rather than in one backend's
     841              :     // own tests.
     842            6 :     let t1 = "2020-01-01T00:00:00.000Z";
     843            6 :     let t2 = "2020-01-02T00:00:00.000Z";
     844            6 :     let first = d
     845            6 :         .record_delivery(a, Kind::Subscription, &sub, t1)
     846            6 :         .await
     847            6 :         .expect("record_delivery")
     848            6 :         .expect("the subscription is there");
     849            6 :     let n1 = &first.doc["notification"];
     850            6 :     assert_eq!(n1["timesSent"], 1, "the first attempt moves timesSent to 1");
     851            6 :     assert_eq!(
     852            6 :         n1["lastNotification"], t1,
     853              :         "lastNotification takes the stamp"
     854              :     );
     855            6 :     assert_eq!(n1["lastSuccess"], t1, "lastSuccess takes the stamp");
     856            6 :     assert_eq!(n1["status"], "ok", "the attempt leaves the notification ok");
     857            6 :     assert!(
     858            6 :         first.doc.get("status").is_none(),
     859              :         "the top-level status is a rendered member, not a stored one: {}",
     860              :         first.doc
     861              :     );
     862            6 :     assert!(
     863            6 :         first.prev_success.is_none(),
     864              :         "a subscription that never succeeded has no previous lastSuccess"
     865              :     );
     866              : 
     867            6 :     let second = d
     868            6 :         .record_delivery(a, Kind::Subscription, &sub, t2)
     869            6 :         .await
     870            6 :         .expect("record_delivery")
     871            6 :         .expect("the subscription is still there");
     872            6 :     assert_eq!(
     873            6 :         second.doc["notification"]["timesSent"], 2,
     874              :         "timesSent counts attempts, it does not reset"
     875              :     );
     876            6 :     assert_eq!(
     877            6 :         second.prev_success.as_ref().and_then(Value::as_str),
     878            6 :         Some(t1),
     879              :         "the overwritten lastSuccess comes back, or a failed attempt cannot \
     880              :          roll it back"
     881              :     );
     882              : 
     883              :     // 5.8.6: a subscription deleted between matching and delivery has no row
     884              :     // to book against, and nothing may be sent.
     885            6 :     assert!(
     886            6 :         d.record_delivery(a, Kind::Subscription, &gone, t1)
     887            6 :             .await
     888            6 :             .expect("record_delivery")
     889            6 :             .is_none(),
     890              :         "an absent subscription books nothing"
     891              :     );
     892              : 
     893              :     // the batched stamp is the same rule, one answer per attempt in
     894              :     // input order, and a batch never books another tenant's row.
     895            6 :     let t3 = "2020-01-03T00:00:00.000Z";
     896            6 :     let batch = d
     897            6 :         .record_deliveries(
     898            6 :             a,
     899            6 :             Kind::Subscription,
     900            6 :             &[(gone.as_str(), t3), (sub.as_str(), t3)],
     901            6 :         )
     902            6 :         .await
     903            6 :         .expect("record_deliveries");
     904            6 :     assert_eq!(batch.len(), 2, "one answer per attempt: {batch:?}");
     905            6 :     assert!(
     906            6 :         batch.first().is_some_and(Option::is_none),
     907              :         "an absent subscription books nothing"
     908              :     );
     909            6 :     let third = batch
     910            6 :         .get(1)
     911            6 :         .and_then(Option::as_ref)
     912            6 :         .expect("the subscription is booked");
     913            6 :     assert_eq!(third.doc["notification"]["timesSent"], 3);
     914            6 :     assert_eq!(third.doc["notification"]["lastSuccess"], t3);
     915            6 :     assert_eq!(
     916            6 :         third.prev_success.as_ref().and_then(Value::as_str),
     917            6 :         Some(t2),
     918              :         "each attempt of a batch gets back the lastSuccess it overwrote"
     919              :     );
     920            6 :     let elsewhere = d
     921            6 :         .record_deliveries(b, Kind::Subscription, &[(sub.as_str(), t3)])
     922            6 :         .await
     923            6 :         .expect("record_deliveries");
     924            6 :     assert!(
     925            6 :         elsewhere.first().is_some_and(Option::is_none),
     926              :         "a batch in another tenant books nothing: {elsewhere:?}"
     927              :     );
     928              : 
     929            6 :     assert!(
     930            6 :         d.delete(a, Kind::Subscription, &sub).await.expect("delete"),
     931              :         "the contract's own subscription must be removable"
     932              :     );
     933              : 
     934              :     // ADR-0021: a stored @context is a Tenant's document. "Hosted" and
     935              :     // "ImplicitlyCreated" rows hold term mappings authored through one
     936              :     // Tenant's requests, and 5.5.7 makes those mappings decide what that
     937              :     // Tenant's payloads mean, so a backend that answered every caller would
     938              :     // hand one Tenant's meaning of its own data to another. "Cached" is a
     939              :     // copy of a public document (5.13.1) and belongs to no Tenant.
     940            6 :     let hosted_id = format!("{prefix}-ctx-hosted");
     941            6 :     let cached_id = format!("{prefix}-ctx-cached");
     942            6 :     let hosted = json!({"localId": hosted_id, "kind": "Hosted", "owner": a.as_str(),
     943            6 :                         "body": {"@context": {"t": "https://example.org/t"}}});
     944            6 :     d.context_put(Some(a), &hosted_id, hosted.clone())
     945            6 :         .await
     946            6 :         .expect("the owning tenant stores its own @context");
     947            6 :     d.context_put(
     948            6 :         None,
     949            6 :         &cached_id,
     950            6 :         json!({"localId": cached_id, "kind": "Cached", "body": {"@context": {}}}),
     951            6 :     )
     952            6 :     .await
     953            6 :     .expect("a Cached copy belongs to no tenant");
     954              : 
     955            6 :     assert!(
     956            6 :         d.context_get(Some(a), &hosted_id)
     957            6 :             .await
     958            6 :             .expect("context_get")
     959            6 :             .is_some(),
     960              :         "the owning tenant must read its own @context"
     961              :     );
     962            6 :     assert!(
     963            6 :         d.context_get(Some(b), &hosted_id)
     964            6 :             .await
     965            6 :             .expect("context_get")
     966            6 :             .is_none(),
     967              :         "another tenant's Hosted @context must be as absent as one never stored"
     968              :     );
     969            6 :     assert!(
     970            6 :         d.context_get(None, &hosted_id)
     971            6 :             .await
     972            6 :             .expect("context_get")
     973            6 :             .is_none(),
     974              :         "no tenant in scope must reach a tenant's @context"
     975              :     );
     976            6 :     assert!(
     977            6 :         d.context_list_meta(Some(b))
     978            6 :             .await
     979            6 :             .expect("context_list_meta")
     980            6 :             .iter()
     981            8 :             .all(|r| r["localId"] != hosted_id.as_str()),
     982              :         "another tenant's @context must not be listed"
     983              :     );
     984            6 :     assert!(
     985            6 :         !d.context_delete(Some(b), &hosted_id)
     986            6 :             .await
     987            6 :             .expect("context_delete"),
     988              :         "another tenant must not delete it"
     989              :     );
     990            6 :     assert!(
     991            6 :         d.context_put(Some(b), &hosted_id, hosted).await.is_err(),
     992              :         "another tenant must not overwrite it either"
     993              :     );
     994            6 :     assert!(
     995            6 :         d.context_get(Some(a), &hosted_id)
     996            6 :             .await
     997            6 :             .expect("context_get")
     998            6 :             .is_some(),
     999              :         "the owner's @context must survive every foreign attempt"
    1000              :     );
    1001           18 :     for t in [Some(a), Some(b), None] {
    1002           18 :         assert!(
    1003           18 :             d.context_get(t, &cached_id)
    1004           18 :                 .await
    1005           18 :                 .expect("context_get")
    1006           18 :                 .is_some(),
    1007              :             "a Cached copy is a public document every tenant reaches"
    1008              :         );
    1009              :     }
    1010            6 :     assert!(
    1011            6 :         d.context_delete(Some(a), &hosted_id)
    1012            6 :             .await
    1013            6 :             .expect("context_delete"),
    1014              :         "the contract's own @context must be removable by its owner"
    1015              :     );
    1016            6 :     assert!(
    1017            6 :         d.context_delete(None, &cached_id)
    1018            6 :             .await
    1019            6 :             .expect("context_delete"),
    1020              :         "and the Cached copy by anyone"
    1021              :     );
    1022              : 
    1023            6 :     context_rows_are_locked(d, a, b, prefix).await;
    1024              : 
    1025           12 :     for id in [&e1, &e2] {
    1026           12 :         assert!(
    1027           12 :             d.delete(a, Kind::Entity, id).await.expect("delete"),
    1028              :             "the contract's own rows must be removable"
    1029              :         );
    1030              :     }
    1031            6 : }
    1032              : 
    1033              : /// Drive two futures to completion together on the calling task, so their
    1034              : /// store calls interleave (the crate carries no executor of its own).
    1035            6 : async fn both<A: std::future::Future, B: std::future::Future>(
    1036            6 :     x: A,
    1037            6 :     y: B,
    1038            6 : ) -> (A::Output, B::Output) {
    1039            6 :     let (mut x, mut y) = (Box::pin(x), Box::pin(y));
    1040            6 :     let (mut xo, mut yo) = (None, None);
    1041         1686 :     std::future::poll_fn(|cx| {
    1042         1686 :         if xo.is_none() {
    1043           37 :             if let std::task::Poll::Ready(v) = std::future::Future::poll(x.as_mut(), cx) {
    1044            6 :                 xo = Some(v);
    1045           31 :             }
    1046         1649 :         }
    1047         1686 :         if yo.is_none() {
    1048         1686 :             if let std::task::Poll::Ready(v) = std::future::Future::poll(y.as_mut(), cx) {
    1049            6 :                 yo = Some(v);
    1050         1680 :             }
    1051            0 :         }
    1052         1686 :         match (xo.take(), yo.take()) {
    1053            6 :             (Some(a), Some(b)) => std::task::Poll::Ready((a, b)),
    1054         1680 :             (a, b) => {
    1055         1680 :                 xo = a;
    1056         1680 :                 yo = b;
    1057         1680 :                 std::task::Poll::Pending
    1058              :             }
    1059              :         }
    1060         1686 :     })
    1061            6 :     .await
    1062            6 : }
    1063              : 
    1064              : /// 5.13.3.5 + ADR-0021 for the @context rows: a row has a kind, and a
    1065              : /// read-modify-write of one holds its lock, so a usage bump racing a
    1066              : /// delete leaves it deleted and two racing bumps both count.
    1067            6 : async fn context_rows_are_locked(
    1068            6 :     d: &dyn CurrentStateDriver,
    1069            6 :     a: &TenantId,
    1070            6 :     b: &TenantId,
    1071            6 :     prefix: &str,
    1072            6 : ) {
    1073            6 :     let id = format!("{prefix}-ctx-bumped");
    1074            6 :     assert!(
    1075            6 :         d.context_put(None, &id, json!({"localId": id, "body": {"@context": {}}}))
    1076            6 :             .await
    1077            6 :             .is_err(),
    1078              :         "a row without a kind must be refused"
    1079              :     );
    1080           30 :     let bump = || -> crate::MutateFn<'static> {
    1081           30 :         Box::new(|row| {
    1082           12 :             let o = row.as_object_mut().ok_or(())?;
    1083           12 :             let n = o.get("numberOfHits").and_then(Value::as_u64).unwrap_or(0);
    1084           12 :             o.insert("numberOfHits".into(), json!(n.saturating_add(1)));
    1085           12 :             Ok(())
    1086           12 :         })
    1087           30 :     };
    1088            6 :     assert!(
    1089            6 :         d.context_mutate(None, &id, bump())
    1090            6 :             .await
    1091            6 :             .expect("context_mutate")
    1092            6 :             .is_none(),
    1093              :         "context_mutate of an absent row must answer None"
    1094              :     );
    1095            6 :     assert!(
    1096            6 :         d.context_get(None, &id)
    1097            6 :             .await
    1098            6 :             .expect("context_get")
    1099            6 :             .is_none(),
    1100              :         "context_mutate of an absent row must not insert it"
    1101              :     );
    1102            6 :     d.context_put(
    1103            6 :         None,
    1104            6 :         &id,
    1105            6 :         json!({"localId": id, "kind": "Cached", "numberOfHits": 0, "body": {"@context": {}}}),
    1106            6 :     )
    1107            6 :     .await
    1108            6 :     .expect("context_put");
    1109            6 :     let (x, y) = both(
    1110            6 :         d.context_mutate(Some(a), &id, bump()),
    1111            6 :         d.context_mutate(Some(b), &id, bump()),
    1112              :     )
    1113            6 :     .await;
    1114            6 :     assert!(
    1115            6 :         matches!((x, y), (Ok(Some(Ok(()))), Ok(Some(Ok(()))))),
    1116              :         "both bumps of a Cached row must apply"
    1117              :     );
    1118            6 :     let hits = d
    1119            6 :         .context_get(None, &id)
    1120            6 :         .await
    1121            6 :         .expect("context_get")
    1122            6 :         .map(|r| r["numberOfHits"].clone());
    1123            6 :     assert_eq!(hits, Some(json!(2)), "two racing bumps must both count");
    1124              : 
    1125            6 :     let hosted = format!("{prefix}-ctx-owned");
    1126            6 :     d.context_put(
    1127            6 :         Some(a),
    1128            6 :         &hosted,
    1129            6 :         json!({"localId": hosted, "kind": "Hosted", "owner": a.as_str(), "body": {"@context": {}}}),
    1130            6 :     )
    1131            6 :     .await
    1132            6 :     .expect("context_put");
    1133            6 :     assert!(
    1134            6 :         d.context_mutate(Some(b), &hosted, bump())
    1135            6 :             .await
    1136            6 :             .expect("context_mutate")
    1137            6 :             .is_none(),
    1138              :         "another tenant's @context must be as absent to a mutate as to a read"
    1139              :     );
    1140            6 :     assert!(
    1141            6 :         d.context_delete(Some(a), &hosted)
    1142            6 :             .await
    1143            6 :             .expect("context_delete"),
    1144              :         "the owner removes its @context"
    1145              :     );
    1146            6 :     assert!(
    1147            6 :         d.context_mutate(Some(a), &hosted, bump())
    1148            6 :             .await
    1149            6 :             .expect("context_mutate")
    1150            6 :             .is_none(),
    1151              :         "a bump after the delete must find nothing"
    1152              :     );
    1153            6 :     assert!(
    1154            6 :         d.context_get(Some(a), &hosted)
    1155            6 :             .await
    1156            6 :             .expect("context_get")
    1157            6 :             .is_none(),
    1158              :         "a bump racing a delete must leave the row deleted"
    1159              :     );
    1160            6 :     assert!(d.context_delete(None, &id).await.expect("context_delete"));
    1161            6 : }
    1162              : 
    1163              : /// Hold a temporal driver to the same shape. A driver that answers
    1164              : /// `supported() == false` declares it keeps no history and is held to
    1165              : /// nothing else.
    1166            6 : pub async fn run_temporal_contract(
    1167            6 :     d: &dyn TemporalDriver,
    1168            6 :     a: &TenantId,
    1169            6 :     b: &TenantId,
    1170            6 :     prefix: &str,
    1171            6 : ) {
    1172            6 :     if !d.supported() {
    1173            0 :         return;
    1174            6 :     }
    1175            6 :     let e1 = format!("urn:ngsi-ld:{prefix}:t1");
    1176            6 :     let gone = format!("urn:ngsi-ld:{prefix}:tabsent");
    1177              : 
    1178            6 :     assert!(
    1179            6 :         d.get(a, &gone).await.expect("get").is_none(),
    1180              :         "get of an absent temporal document must answer None"
    1181              :     );
    1182            6 :     assert!(
    1183            6 :         !d.delete(a, &gone).await.expect("delete"),
    1184              :         "delete of an absent temporal document must answer false"
    1185              :     );
    1186            6 :     let missed = d
    1187            6 :         .mutate::<(), ()>(a, &gone, |_| Ok(()))
    1188            6 :         .await
    1189            6 :         .expect("mutate");
    1190            6 :     assert!(
    1191            6 :         missed.is_none(),
    1192              :         "mutate of an absent document must answer None"
    1193              :     );
    1194            6 :     assert!(
    1195            6 :         d.get(a, &gone).await.expect("get").is_none(),
    1196              :         "mutate of an absent document must not insert it (047_06)"
    1197              :     );
    1198              : 
    1199            6 :     assert!(
    1200            6 :         d.create(a, &e1, doc(&e1)).await.expect("create"),
    1201              :         "the first create reports true"
    1202              :     );
    1203            6 :     assert!(
    1204            6 :         !d.create(a, &e1, doc(&e1)).await.expect("create"),
    1205              :         "a create over an existing id reports false"
    1206              :     );
    1207            6 :     let rejected = d
    1208            6 :         .mutate::<(), &'static str>(a, &e1, |v| {
    1209            6 :             v["marker"] = json!(2);
    1210            6 :             Err("rejected")
    1211            6 :         })
    1212            6 :         .await
    1213            6 :         .expect("mutate");
    1214            6 :     assert_eq!(rejected, Some(Err("rejected")));
    1215            6 :     assert!(
    1216            6 :         d.get(a, &e1)
    1217            6 :             .await
    1218            6 :             .expect("get")
    1219            6 :             .expect("row")
    1220            6 :             .get("marker")
    1221            6 :             .is_none(),
    1222              :         "a rejected mutate must commit nothing"
    1223              :     );
    1224              : 
    1225            6 :     assert!(
    1226            6 :         d.get(b, &e1).await.expect("get").is_none(),
    1227              :         "another tenant must not read the temporal document"
    1228              :     );
    1229            6 :     assert!(
    1230            6 :         !d.delete(b, &e1).await.expect("delete"),
    1231              :         "another tenant must not delete it"
    1232              :     );
    1233            6 :     assert!(
    1234            6 :         d.list(b)
    1235            6 :             .await
    1236            6 :             .expect("list")
    1237            6 :             .iter()
    1238            6 :             .all(|r| r["id"] != e1.as_str()),
    1239              :         "another tenant must not list it"
    1240              :     );
    1241              : 
    1242            6 :     assert!(
    1243            6 :         d.delete(a, &e1).await.expect("delete"),
    1244              :         "the contract's own rows must be removable"
    1245              :     );
    1246            6 : }
    1247              : 
    1248              : /// Hold a driver to tenant isolation: `a` and `b` each hold an
    1249              : /// Entity, a Subscription, a Registration and a temporal document under the
    1250              : /// SAME ids, and nothing done as `a` reads, changes or deletes `b`'s copy.
    1251              : /// The walk covers every tenant-taking family the API reaches: one-row and
    1252              : /// batch writes, the three list reads, the query, delivery bookkeeping, the
    1253              : /// registration lookup, the temporal reads and the drain, and last a purge
    1254              : /// of `a`, which must leave `b` whole.
    1255              : ///
    1256              : /// The caller picks the pair that matters for its backend: two names a
    1257              : /// LIKE pattern would confuse (`a_b`, `axb`), the default Tenant beside a
    1258              : /// named one, a name that extends the other. `a` is purged, so it must not
    1259              : /// be a Tenant other data lives in. `cs` and `tp` may be the same store.
    1260           24 : pub async fn run_tenant_isolation_contract(
    1261           24 :     cs: &dyn CurrentStateDriver,
    1262           24 :     tp: &dyn TemporalDriver,
    1263           24 :     a: &TenantId,
    1264           24 :     b: &TenantId,
    1265           24 :     prefix: &str,
    1266           24 : ) {
    1267              :     use crate::filter::{EntityFilter, TemporalFilter};
    1268           24 :     assert_ne!(a, b, "the walk needs two Tenants");
    1269           24 :     let e = format!("urn:ngsi-ld:{prefix}:shared");
    1270           24 :     let sub = format!("urn:ngsi-ld:Subscription:{prefix}:shared");
    1271           24 :     let reg = format!("urn:ngsi-ld:ContextSourceRegistration:{prefix}:shared");
    1272           24 :     let ty = "https://uri.etsi.org/ngsi-ld/default-context/T";
    1273          312 :     let mine = |t: &TenantId| json!({"id": e, "type": ty, "who": t.as_str()});
    1274           24 :     let ids = [e.clone()];
    1275           24 :     let ids_ref = [e.as_str()];
    1276              : 
    1277           48 :     for t in [a, b] {
    1278           48 :         assert!(
    1279           48 :             cs.create(t, Kind::Entity, &e, mine(t))
    1280           48 :                 .await
    1281           48 :                 .expect("create"),
    1282              :             "{}: the shared id is new in each Tenant",
    1283            0 :             t.as_str()
    1284              :         );
    1285           48 :         cs.create(
    1286           48 :             t,
    1287           48 :             Kind::Subscription,
    1288           48 :             &sub,
    1289           48 :             json!({"id": sub, "type": "Subscription", "who": t.as_str()}),
    1290           48 :         )
    1291           48 :         .await
    1292           48 :         .expect("subscription");
    1293           48 :         cs.create(
    1294           48 :             t,
    1295           48 :             Kind::Registration,
    1296           48 :             &reg,
    1297           48 :             json!({"id": reg, "type": "ContextSourceRegistration", "who": t.as_str(),
    1298           48 :                    "information": [{"entities": [{"type": ty}]}], "endpoint": "http://cs.example"}),
    1299           48 :         )
    1300           48 :         .await
    1301           48 :         .expect("registration");
    1302              :     }
    1303              :     // `b`'s copies as they stand; every step below compares against these.
    1304           24 :     let b_entity = cs.get(b, Kind::Entity, &e).await.expect("get");
    1305           24 :     let b_sub = cs.get(b, Kind::Subscription, &sub).await.expect("get");
    1306              :     // A macro, not a closure: the check awaits, and an async closure that
    1307              :     // borrows the ids cannot be called twice.
    1308              :     macro_rules! b_unchanged {
    1309              :         ($step:literal) => {
    1310              :             assert_eq!(
    1311              :                 cs.get(b, Kind::Entity, &e).await.expect("get"),
    1312              :                 b_entity,
    1313              :                 "{} as {} changed {}'s Entity",
    1314              :                 $step,
    1315              :                 a.as_str(),
    1316              :                 b.as_str()
    1317              :             );
    1318              :             assert_eq!(
    1319              :                 cs.get(b, Kind::Subscription, &sub).await.expect("get"),
    1320              :                 b_sub,
    1321              :                 "{} as {} changed {}'s Subscription",
    1322              :                 $step,
    1323              :                 a.as_str(),
    1324              :                 b.as_str()
    1325              :             );
    1326              :         };
    1327              :     }
    1328           96 :     let only_a = |rows: &[Value], what: &str| {
    1329           96 :         let shared: Vec<&Value> = rows.iter().filter(|r| r["id"] == e.as_str()).collect();
    1330           96 :         assert_eq!(shared.len(), 1, "{what}: the shared id once, {rows:?}");
    1331           96 :         assert_eq!(
    1332           96 :             shared[0]["who"],
    1333           96 :             a.as_str(),
    1334              :             "{what} served another Tenant's copy"
    1335              :         );
    1336           96 :     };
    1337              : 
    1338              :     // Reads.
    1339           24 :     let got = cs
    1340           24 :         .get(a, Kind::Entity, &e)
    1341           24 :         .await
    1342           24 :         .expect("get")
    1343           24 :         .expect("row");
    1344           24 :     assert_eq!(got["who"], a.as_str(), "get served another Tenant's copy");
    1345           24 :     only_a(&cs.list(a, Kind::Entity).await.expect("list"), "list");
    1346           24 :     only_a(
    1347           24 :         &cs.list_page(a, Kind::Entity, None, 1000)
    1348           24 :             .await
    1349           24 :             .expect("page"),
    1350           24 :         "list_page",
    1351              :     );
    1352           24 :     only_a(
    1353           24 :         &cs.list_slice(a, Kind::Entity, 0, 1000)
    1354           24 :             .await
    1355           24 :             .expect("slice")
    1356              :             .0,
    1357           24 :         "list_slice",
    1358              :     );
    1359           24 :     let q = cs
    1360           24 :         .query_entities(
    1361           24 :             a,
    1362           24 :             &EntityFilter {
    1363           24 :                 ids: Some(&ids_ref),
    1364           24 :                 ..Default::default()
    1365           24 :             },
    1366           24 :         )
    1367           24 :         .await
    1368           24 :         .expect("query");
    1369           24 :     only_a(&q.rows, "query_entities");
    1370           24 :     let subs = cs.list(a, Kind::Subscription).await.expect("list");
    1371           24 :     assert!(
    1372           24 :         subs.iter()
    1373           24 :             .filter(|s| s["id"] == sub.as_str())
    1374           24 :             .all(|s| s["who"] == a.as_str()),
    1375              :         "the Subscription list served another Tenant's copy: {subs:?}"
    1376              :     );
    1377           24 :     let regs = cs
    1378           24 :         .matching_registrations(a, None, None)
    1379           24 :         .await
    1380           24 :         .expect("registrations");
    1381           24 :     let shared: Vec<&Value> = regs.iter().filter(|r| r["id"] == reg.as_str()).collect();
    1382           24 :     assert!(
    1383           24 :         shared.len() == 1 && shared[0]["who"] == a.as_str(),
    1384              :         "matching_registrations served another Tenant's copy: {regs:?}"
    1385              :     );
    1386              : 
    1387              :     // Writes, one by one and batched.
    1388           24 :     cs.mutate::<(), ()>(a, Kind::Entity, &e, |d| {
    1389           24 :         d["touched"] = json!(true);
    1390           24 :         Ok(())
    1391           24 :     })
    1392           24 :     .await
    1393           24 :     .expect("mutate");
    1394           24 :     b_unchanged!("mutate");
    1395           24 :     cs.upsert(
    1396           24 :         a,
    1397           24 :         Kind::Entity,
    1398           24 :         &e,
    1399           24 :         json!({"id": e, "type": ty, "who": a.as_str(), "n": 1}),
    1400           24 :     )
    1401           24 :     .await
    1402           24 :     .expect("upsert");
    1403           24 :     b_unchanged!("upsert");
    1404           24 :     assert_eq!(
    1405           24 :         cs.batch_create(a, vec![(e.clone(), mine(a))])
    1406           24 :             .await
    1407           24 :             .expect("batch_create"),
    1408              :         [false],
    1409              :         "a batch create sees only its own Tenant's row"
    1410              :     );
    1411           24 :     b_unchanged!("batch_create");
    1412           24 :     assert_eq!(
    1413           24 :         cs.batch_upsert(a, vec![(e.clone(), mine(a))])
    1414           24 :             .await
    1415           24 :             .expect("batch_upsert"),
    1416              :         [false],
    1417              :         "a batch upsert replaces its own Tenant's row"
    1418              :     );
    1419           24 :     b_unchanged!("batch_upsert");
    1420           24 :     cs.batch_mutate::<()>(a, &ids, |_, d| {
    1421           24 :         d["batched"] = json!(true);
    1422           24 :         Ok(())
    1423           24 :     })
    1424           24 :     .await
    1425           24 :     .expect("batch_mutate");
    1426           24 :     b_unchanged!("batch_mutate");
    1427           24 :     cs.record_delivery(a, Kind::Subscription, &sub, "2026-10-11T00:00:00Z")
    1428           24 :         .await
    1429           24 :         .expect("record_delivery")
    1430           24 :         .expect("a's Subscription");
    1431           24 :     b_unchanged!("record_delivery");
    1432           24 :     assert!(
    1433           24 :         cs.delete_entity_if(a, &e, &|_| true)
    1434           24 :             .await
    1435           24 :             .expect("delete_entity_if"),
    1436              :         "a conditional delete removes its own Tenant's row"
    1437              :     );
    1438           24 :     b_unchanged!("delete_entity_if");
    1439           24 :     cs.create(a, Kind::Entity, &e, mine(a))
    1440           24 :         .await
    1441           24 :         .expect("recreate");
    1442           24 :     assert_eq!(
    1443           24 :         cs.batch_delete(a, &ids).await.expect("batch_delete"),
    1444              :         [true],
    1445              :         "a batch delete removes its own Tenant's row"
    1446              :     );
    1447           24 :     b_unchanged!("batch_delete");
    1448           24 :     cs.create(a, Kind::Entity, &e, mine(a))
    1449           24 :         .await
    1450           24 :         .expect("recreate");
    1451           24 :     assert!(cs.delete(a, Kind::Entity, &e).await.expect("delete"));
    1452           24 :     b_unchanged!("delete");
    1453           24 :     cs.create(a, Kind::Entity, &e, mine(a))
    1454           24 :         .await
    1455           24 :         .expect("recreate");
    1456              : 
    1457              :     // History under the same id.
    1458           24 :     if tp.supported() {
    1459           48 :         let instance = |t: &TenantId| {
    1460           48 :             json!({"n": [{"type": "Property", "value": t.as_str(),
    1461           48 :                           "observedAt": "2026-10-11T00:00:00Z",
    1462           48 :                           "instanceId": format!("urn:ngsi-ld:{prefix}:{}", t.as_str())}]})
    1463           48 :         };
    1464           48 :         for t in [a, b] {
    1465           48 :             tp.create(t, &e, mine(t)).await.expect("temporal create");
    1466           48 :             tp.temporal_append(t, &e, &mine(t), &instance(t))
    1467           48 :                 .await
    1468           48 :                 .expect("append");
    1469              :         }
    1470           24 :         let b_history = tp.get(b, &e).await.expect("get");
    1471              :         macro_rules! history_unchanged {
    1472              :             ($step:literal) => {
    1473              :                 assert_eq!(
    1474              :                     tp.get(b, &e).await.expect("get"),
    1475              :                     b_history,
    1476              :                     "{} as {} changed {}'s history",
    1477              :                     $step,
    1478              :                     a.as_str(),
    1479              :                     b.as_str()
    1480              :                 );
    1481              :             };
    1482              :         }
    1483           48 :         let values = |doc: &Value| doc["n"].to_string();
    1484           24 :         let one = tp
    1485           24 :             .get_temporal(a, &e, &TemporalFilter::default())
    1486           24 :             .await
    1487           24 :             .expect("get_temporal")
    1488           24 :             .expect("a's history");
    1489           24 :         assert!(
    1490           24 :             !values(&one).contains(&format!("\"{}\"", b.as_str())),
    1491              :             "get_temporal served another Tenant's instances: {one}"
    1492              :         );
    1493           24 :         let rows = tp
    1494           24 :             .query_temporal(
    1495           24 :                 a,
    1496           24 :                 &TemporalFilter {
    1497           24 :                     ids: Some(&ids_ref),
    1498           24 :                     ..Default::default()
    1499           24 :                 },
    1500           24 :             )
    1501           24 :             .await
    1502           24 :             .expect("query_temporal")
    1503              :             .rows;
    1504           24 :         assert_eq!(
    1505           24 :             rows.len(),
    1506              :             1,
    1507              :             "query_temporal: the shared id once, {rows:?}"
    1508              :         );
    1509           24 :         assert!(
    1510           24 :             !values(&rows[0]).contains(&format!("\"{}\"", b.as_str())),
    1511              :             "query_temporal served another Tenant's instances: {rows:?}"
    1512              :         );
    1513           24 :         assert!(
    1514           24 :             tp.list(a)
    1515           24 :                 .await
    1516           24 :                 .expect("list")
    1517           24 :                 .iter()
    1518           24 :                 .filter(|r| r["id"] == e.as_str())
    1519           24 :                 .count()
    1520              :                 == 1,
    1521              :             "the temporal list holds the shared id once"
    1522              :         );
    1523           24 :         tp.event_list(&[crate::TemporalEvent {
    1524           24 :             op: crate::TemporalOp::AttrModified,
    1525           24 :             tenant: a.clone(),
    1526           24 :             entity_id: e.clone(),
    1527           24 :             shell: mine(a),
    1528           24 :             attr: "n".into(),
    1529           24 :             instance: json!({"type": "Property", "value": "drained",
    1530           24 :                              "observedAt": "2026-10-11T00:01:00Z",
    1531           24 :                              "instanceId": format!("urn:ngsi-ld:{prefix}:drained")}),
    1532           24 :         }])
    1533           24 :         .await
    1534           24 :         .expect("event_list");
    1535           24 :         history_unchanged!("event_list");
    1536           24 :         tp.mutate::<(), ()>(a, &e, |d| {
    1537           24 :             d["touched"] = json!(true);
    1538           24 :             Ok(())
    1539           24 :         })
    1540           24 :         .await
    1541           24 :         .expect("mutate");
    1542           24 :         history_unchanged!("temporal mutate");
    1543           24 :         assert!(tp.delete(a, &e).await.expect("delete"));
    1544           24 :         history_unchanged!("temporal delete");
    1545           24 :         tp.create(a, &e, mine(a)).await.expect("recreate");
    1546           24 :         tp.purge_tenant(a).await.expect("temporal purge");
    1547           24 :         assert!(
    1548           24 :             tp.get(a, &e).await.expect("get").is_none(),
    1549              :             "the purge empties a's history"
    1550              :         );
    1551           24 :         history_unchanged!("temporal purge");
    1552           24 :         assert!(tp.delete(b, &e).await.expect("cleanup"));
    1553            0 :     }
    1554              : 
    1555              :     // A purge of `a` leaves `b` whole.
    1556           24 :     match cs.purge_tenant(a).await {
    1557              :         Ok(_) => {
    1558           24 :             assert!(
    1559           24 :                 cs.get(a, Kind::Entity, &e).await.expect("get").is_none(),
    1560              :                 "the purge empties a"
    1561              :             );
    1562           24 :             b_unchanged!("purge_tenant");
    1563           24 :             assert!(
    1564           24 :                 cs.get(b, Kind::Registration, &reg)
    1565           24 :                     .await
    1566           24 :                     .expect("get")
    1567           24 :                     .is_some(),
    1568              :                 "the purge of {} took {}'s Registration",
    1569            0 :                 a.as_str(),
    1570            0 :                 b.as_str()
    1571              :             );
    1572              :         }
    1573              :         Err(antares_model::NgsiError::OperationNotSupported(_)) => {
    1574            0 :             for kind in [Kind::Entity, Kind::Subscription, Kind::Registration] {
    1575            0 :                 let id = match kind {
    1576            0 :                     Kind::Entity => &e,
    1577            0 :                     Kind::Subscription => &sub,
    1578            0 :                     _ => &reg,
    1579              :                 };
    1580            0 :                 cs.delete(a, kind, id).await.expect("cleanup");
    1581              :             }
    1582              :         }
    1583            0 :         Err(other) => panic!("purge_tenant: {other:?}"),
    1584              :     }
    1585           72 :     for (kind, id) in [
    1586           24 :         (Kind::Entity, &e),
    1587           24 :         (Kind::Subscription, &sub),
    1588           24 :         (Kind::Registration, &reg),
    1589           24 :     ] {
    1590           72 :         assert!(
    1591           72 :             cs.delete(b, kind, id).await.expect("cleanup"),
    1592              :             "{}'s {kind:?} survived",
    1593            0 :             b.as_str()
    1594              :         );
    1595              :     }
    1596           24 : }
        

Generated by: LCOV version 2.0-1