LCOV - code coverage report
Current view: top level - antares-api/src - lib.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 99.7 % 1345 1341
Test Date: 2026-11-02 13:21:27 Functions: 72.2 % 478 345

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! NGSI-LD HTTP binding: axum routers, thin
       3              : //! handlers per spec operation.
       4              : // a panic on a request path is a remote DoS. Outside tests
       5              : // this crate may not unwrap, expect, panic, index or overflow.
       6              : #![cfg_attr(
       7              :     not(test),
       8              :     deny(
       9              :         clippy::unwrap_used,
      10              :         clippy::expect_used,
      11              :         clippy::panic,
      12              :         clippy::todo,
      13              :         clippy::unimplemented,
      14              :         clippy::dbg_macro,
      15              :         clippy::print_stdout,
      16              :         clippy::print_stderr,
      17              :         clippy::indexing_slicing,
      18              :         clippy::arithmetic_side_effects
      19              :     )
      20              : )]
      21              : #![cfg_attr(test, allow(clippy::unwrap_used))]
      22              : // the Send proof of the spawned snapshot fill walks temporal → entity map
      23              : // futures deeper than the default 128 (nightly: recursion_depth_exceeding_limit)
      24              : #![recursion_limit = "256"]
      25              : 
      26              : /// Ask the policy engine about one operation (ADR-0020), where the request
      27              : /// enters its handler:
      28              : ///
      29              : /// ```ignore
      30              : /// gate!(st, &tenant, headers, "5.6.6", ids: &[&id]).await?;
      31              : /// ```
      32              : ///
      33              : /// The named members go into the [`policy::Operation`]; every other member
      34              : /// keeps the empty default of `Operation::new`. The awaited value is the
      35              : /// `Filter` the engine narrowed the operation to, shared (`Arc`) so a
      36              : /// view's compiled narrowing reaches the handler uncopied, or the refusal that
      37              : /// becomes a 403. It is a macro because a gate reads as one line at the
      38              : /// call site and the alternative is the same eight lines in every handler.
      39              : ///
      40              : /// With no engine attached there is nothing to ask, so nothing is built:
      41              : /// the operation, the subject and its headers, the boxed future and the
      42              : /// [`policy::TIMEOUT`] timer all belong to the arm that has an engine to
      43              : /// ask. A broker running the default pays one branch per operation.
      44              : macro_rules! gate {
      45              :     ($st:expr, $tenant:expr, $headers:expr, $clause:expr $(, $field:ident: $value:expr)* $(,)?) => {
      46        53622 :         async {
      47        53622 :             let answer = match &$st.policy {
      48        50246 :                 None => Ok($crate::policy::unnarrowed()),
      49         3376 :                 Some(engine) => {
      50         3376 :                     $crate::policy::gate(
      51         3376 :                         engine.as_ref(),
      52         3376 :                         &$crate::snapshots::asking_tenant(&$st, $tenant).await,
      53              :                         $headers,
      54         3376 :                         &$crate::policy::Operation {
      55         3376 :                             $($field: $value,)*
      56         3376 :                             ..$crate::policy::Operation::new($clause)
      57         3376 :                         },
      58              :                     )
      59         3376 :                     .await
      60         3376 :                     .map(std::sync::Arc::new)
      61              :                 }
      62              :             };
      63        53622 :             answer
      64        53622 :         }
      65              :     };
      66              : }
      67              : 
      68              : pub mod admin_auth;
      69              : pub(crate) mod attrs;
      70              : pub(crate) mod batch;
      71              : pub mod bounds;
      72              : pub mod conformance;
      73              : pub(crate) mod contexts;
      74              : pub(crate) mod csource;
      75              : pub(crate) mod distsub;
      76              : pub mod egress;
      77              : pub(crate) mod entities;
      78              : pub(crate) mod entity_map;
      79              : pub(crate) mod entity_maps;
      80              : pub(crate) mod federation;
      81              : // The request parsers for the fuzz targets; never in a broker build.
      82              : #[cfg(feature = "fuzzing")]
      83              : pub mod fuzzing;
      84              : pub mod history;
      85              : pub mod mirror;
      86              : pub mod negotiate;
      87              : pub mod notify;
      88              : pub(crate) mod paging;
      89              : pub mod policy;
      90              : pub mod provenance;
      91              : pub(crate) mod registry;
      92              : pub(crate) mod repr;
      93              : /// The one GeoJSON geometry rule, for the surfaces that write GeoJSON.
      94              : pub use repr::{geometry_of, rewound};
      95              : mod ops;
      96              : mod router;
      97              : pub mod secret;
      98              : pub(crate) mod snapshots;
      99              : pub(crate) mod stamp;
     100              : pub mod state;
     101              : pub(crate) mod subscriptions;
     102              : pub(crate) mod temporal;
     103              : pub(crate) mod temporalq;
     104              : #[cfg(test)]
     105              : mod tests;
     106              : pub(crate) mod types_attrs;
     107              : 
     108              : pub use antares_notifier::DeliveryPolicy;
     109              : // The sink contract, so a surface reaches it through this crate and names no
     110              : // antares-notifier (rule M2).
     111              : pub use antares_notifier::{
     112              :     build_message, DeliveryError, DeliveryFuture, NotificationSink, Outbound,
     113              : };
     114              : pub use antares_store::{HistoryScope, ShortHistory, HISTORY_RETENTION, SHORT_HISTORY};
     115              : pub use contexts::public_base;
     116              : pub use federation::{
     117              :     AdmitFuture, BuiltForward, ForwardAnswer, ForwardFuture, ForwardTransport, NotifyFuture,
     118              : };
     119              : pub use ops::*;
     120              : pub use router::*;
     121              : pub use state::{AppState, HistoryRules, OnChange, Profile, RecordMode, Recording, TemporalRecord};
     122              : 
     123              : /// Spawn for both targets — tokio natively, the JS microtask queue on
     124              : /// wasm32 (no tokio runtime exists in a browser). Call sites are identical;
     125              : /// only the executor differs. Send is required natively (worker threads) and
     126              : /// meaningless on single-threaded wasm.
     127              : #[cfg(not(target_arch = "wasm32"))]
     128         6164 : pub fn spawn<F>(fut: F)
     129         6164 : where
     130         6164 :     F: std::future::Future<Output = ()> + Send + 'static,
     131              : {
     132         6164 :     BACKGROUND.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
     133         6164 :     let counted = Counted;
     134         6164 :     tokio::spawn(async move {
     135         5906 :         let _counted = counted;
     136         5906 :         fut.await;
     137         5886 :     });
     138         6164 : }
     139              : 
     140              : /// One [`BACKGROUND`] task, uncounted when it ends however it ends: a task
     141              : /// that panicked or was cancelled left the count up and every later
     142              : /// shutdown drain waiting for its whole deadline.
     143              : #[cfg(not(target_arch = "wasm32"))]
     144              : struct Counted;
     145              : 
     146              : #[cfg(not(target_arch = "wasm32"))]
     147              : impl Drop for Counted {
     148         6164 :     fn drop(&mut self) {
     149         6164 :         BACKGROUND.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
     150         6164 :     }
     151              : }
     152              : 
     153              : /// Work a request left behind after its response — the remote leg of a
     154              : /// distributed subscription, an initial Context Source notification, a
     155              : /// forwarded notification, a retry. Counted so a shutdown drain waits for
     156              : /// it: a rolling update that killed these tasks left the chain half-built.
     157              : static BACKGROUND: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
     158              : 
     159              : /// Request-born tasks still running.
     160           44 : pub fn background_tasks() -> usize {
     161           44 :     BACKGROUND.load(std::sync::atomic::Ordering::SeqCst)
     162           44 : }
     163              : 
     164              : /// A task that lives as long as the process (the matcher drain, the
     165              : /// interval tick): never counted, or a drain would wait on it forever.
     166              : #[cfg(not(target_arch = "wasm32"))]
     167         2568 : pub(crate) fn spawn_loop<F>(fut: F)
     168         2568 : where
     169         2568 :     F: std::future::Future<Output = ()> + Send + 'static,
     170              : {
     171         2568 :     tokio::spawn(fut);
     172         2568 : }
     173              : 
     174              : #[cfg(target_arch = "wasm32")]
     175              : pub(crate) fn spawn_loop<F>(fut: F)
     176              : where
     177              :     F: std::future::Future<Output = ()> + 'static,
     178              : {
     179              :     wasm_bindgen_futures::spawn_local(fut);
     180              : }
     181              : 
     182              : #[cfg(target_arch = "wasm32")]
     183              : pub fn spawn<F>(fut: F)
     184              : where
     185              :     F: std::future::Future<Output = ()> + 'static,
     186              : {
     187              :     wasm_bindgen_futures::spawn_local(fut);
     188              : }
     189              : 
     190              : /// The browser build's notification channel: re-exported from the HTTP
     191              : /// binding, where it belongs — the page sink is how that binding delivers
     192              : /// when the runtime has no inbound socket.
     193              : #[cfg(target_arch = "wasm32")]
     194              : pub use antares_notifier::http::page_sink;
     195              : 
     196              : use antares_model::{NgsiError, TenantId, API_ROOT};
     197              : use axum::http::{HeaderMap, StatusCode};
     198              : use axum::response::{IntoResponse, Response};
     199              : use axum::routing::{delete, get, patch, post, MethodRouter};
     200              : use axum::Router;
     201              : use negotiate::{echo_tenant, respond, tenant_from, ApiError};
     202              : 
     203              : /// 5.5.4 Fragment applied to a stored document resource — a Subscription
     204              : /// (5.8.2), a Context Source Registration (5.9.3) or a Context Source
     205              : /// Subscription (5.11.4). Each member of the already-normalized Fragment
     206              : /// replaces the target's; a member that normalization left as JSON null is
     207              : /// the removal form and takes the member out; `id` is never touched, and the
     208              : /// write stamps `modifiedAt` (4.8). Entities do NOT go through here: 5.5.12
     209              : /// Merge Patch descends into Attribute instances and value objects, which is
     210              : /// `entities::merge_instance`.
     211          226 : pub(crate) fn apply_doc_fragment(
     212          226 :     target: &mut serde_json::Map<String, serde_json::Value>,
     213          226 :     fragment: &serde_json::Map<String, serde_json::Value>,
     214          226 :     ts: &str,
     215          226 : ) {
     216          264 :     for (k, v) in fragment {
     217          264 :         if k == "id" {
     218            0 :             continue;
     219          264 :         }
     220          264 :         if v.is_null() {
     221           10 :             target.remove(k);
     222          254 :         } else {
     223          254 :             target.insert(k.clone(), v.clone());
     224          254 :         }
     225              :     }
     226          226 :     target.insert(
     227          226 :         "modifiedAt".into(),
     228          226 :         serde_json::Value::String(ts.to_owned()),
     229              :     );
     230          226 : }
     231              : 
     232              : pub(crate) mod surface;
     233              : pub use surface::ApiSurface;
     234              : 
     235              : pub use antares_ql::scope::scope_matches;
     236              : 
     237              : /// Installs the in-process pipeline on a state: the subscription mirror and
     238              : /// matcher (5.8.6), the interval firing, and the consumer half of a
     239              : /// distributed subscription (5.8.1.4) — a notification arriving on the
     240              : /// internal endpoint is handed to `distsub` from here, so the delivery path
     241              : /// never names it. Every root that serves requests calls this once.
     242         1284 : pub async fn wire(state: &mut AppState) {
     243         1284 :     notify::wire_matcher(state).await;
     244         1284 :     install_csource_notification(state);
     245         1284 : }
     246              : 
     247              : /// The 5.8.1.4 consumer half on its own, for a root that wires its matcher
     248              : /// some other way. A distributed Subscription forwards no copy until this
     249              : /// handler exists: the internal Context Source Registration Subscription
     250              : /// notifies to `urn:antares:distsub:…`, and the delivery path drops that
     251              : /// notification when nothing is installed to take it.
     252         1296 : pub fn install_csource_notification(state: &mut AppState) {
     253         1296 :     state.csource_notification = Some(std::sync::Arc::new(|st, tenant, own_id, reason, regs| {
     254          104 :         Box::pin(distsub::on_csource_notification(
     255          104 :             st, tenant, own_id, reason, regs,
     256          104 :         ))
     257          104 :     }));
     258         1296 : }
     259              : 
     260              : /// 4.22 for the documents that carry their own `expiresAt`: Registrations,
     261              : /// Snapshots and EntityMaps, and the leased Subscriptions of socket
     262              : /// connections. The store's own sweep reaps Entities, which is
     263              : /// what 4.22 names; these three are refused by every read once their expiry
     264              : /// passes but nothing collected them, so they stayed for the life of the
     265              : /// broker. Each kind is reaped by the predicate its own reader uses, so the
     266              : /// sweep removes exactly what a read already hides — never more.
     267              : ///
     268              : /// Opportunistic by construction: a driver that cannot enumerate its
     269              : /// tenants reaps nothing and the reads keep hiding what is left, and a
     270              : /// tenant whose walk fails is skipped rather than failing the tick.
     271              : ///
     272              : /// The walk is the client tenants. A Snapshot's frozen copy carries its own
     273              : /// expiry on the Snapshot document, and expiring it drops the whole synthetic
     274              : /// tenant (`snap_remove`), so there is nothing left inside for a per-document
     275              : /// reaper to find.
     276         1972 : pub async fn sweep_expired_docs(st: &AppState) -> usize {
     277         1972 :     let Ok(tenants) = st.store.tenant_ids().await else {
     278            0 :         return 0;
     279              :     };
     280         1972 :     let mut n: usize = 0;
     281       145058 :     for name in tenants {
     282       145058 :         let Ok(tenant) = antares_model::TenantId::new(&name) else {
     283            0 :             continue;
     284              :         };
     285              :         // Each sweep counts documents it deleted, far below `usize::MAX`.
     286       145058 :         n = n
     287       145058 :             .saturating_add(csource::sweep_expired_registrations(st, &tenant).await)
     288       145058 :             .saturating_add(snapshots::sweep_expired_snapshots(st, &tenant).await)
     289       145058 :             .saturating_add(entity_map::sweep_expired_maps(st, &tenant).await)
     290       145058 :             .saturating_add(subscriptions::sweep_expired_socket_subscriptions(st, &tenant).await);
     291              :     }
     292         1972 :     n
     293         1972 : }
     294              : 
     295              : /// An `AppState` with the notification pipeline installed, for tests that
     296              : /// exercise a route which notifies. The router does not wire it: a caller
     297              : /// that only reads never pays for the mirror seed.
     298              : #[cfg(any(test, feature = "test-kit"))]
     299           84 : pub async fn wired_state(host_alias: &str) -> AppState {
     300           84 :     let mut st = AppState::new(host_alias.to_owned());
     301           84 :     wire(&mut st).await;
     302           84 :     st
     303           84 : }
     304              : 
     305              : #[cfg(test)]
     306              : mod clause_5_2_8 {
     307              :     use super::*;
     308              :     use axum::body::Body;
     309              :     use axum::http::{Request, StatusCode};
     310              :     use tower::ServiceExt;
     311              : 
     312           20 :     async fn post_reg(entities: serde_json::Value) -> StatusCode {
     313           20 :         let app = router(AppState::new("t528".into()));
     314           20 :         let body = serde_json::json!({
     315           20 :             "id": format!("urn:ngsi-ld:ContextSourceRegistration:528-{}",
     316           20 :                           entities.to_string().len()),
     317           20 :             "type": "ContextSourceRegistration",
     318           20 :             "information": [{"entities": entities}],
     319           20 :             "endpoint": "http://cs.example.org:1026"
     320              :         })
     321           20 :         .to_string();
     322           20 :         let req = Request::post("/ngsi-ld/v1/csourceRegistrations")
     323           20 :             .header("Content-Type", "application/json")
     324           20 :             .header("Content-Length", body.len())
     325           20 :             .body(Body::from(body))
     326           20 :             .expect("req");
     327           20 :         app.oneshot(req).await.expect("resp").status()
     328           20 :     }
     329              : 
     330              :     /// Table 5.2.8-1: type is "String or String[]" — the array form must be
     331              :     /// accepted; id must be a URI; idPattern must be a valid regex.
     332              :     #[tokio::test]
     333            4 :     async fn entity_info_type_accepts_the_array_form() {
     334            4 :         assert_eq!(
     335            4 :             post_reg(serde_json::json!([{"type": ["Building", "Vehicle"]}])).await,
     336              :             StatusCode::CREATED,
     337              :             "String[] type is legal"
     338              :         );
     339            4 :         assert_eq!(
     340            4 :             post_reg(serde_json::json!([{"type": "Building"}])).await,
     341              :             StatusCode::CREATED
     342              :         );
     343            4 :         assert_eq!(
     344            4 :             post_reg(serde_json::json!([{"type": [] }])).await,
     345              :             StatusCode::BAD_REQUEST,
     346              :             "an empty type array names no Entity Type"
     347              :         );
     348            4 :         assert_eq!(
     349            4 :             post_reg(serde_json::json!([{"type": "Building", "id": "not a uri"}])).await,
     350              :             StatusCode::BAD_REQUEST
     351              :         );
     352            4 :         assert_eq!(
     353            4 :             post_reg(serde_json::json!([{"type": "Building", "idPattern": "urn:[" }])).await,
     354            4 :             StatusCode::BAD_REQUEST
     355            4 :         );
     356            4 :     }
     357              : }
     358              : 
     359              : #[cfg(test)]
     360              : mod clause_5_2_9 {
     361              :     use super::*;
     362              :     use axum::body::Body;
     363              :     use axum::http::{Request, StatusCode};
     364              :     use tower::ServiceExt;
     365              : 
     366           68 :     async fn post_reg(extra: serde_json::Value) -> StatusCode {
     367           68 :         let app = router(AppState::new("t529".into()));
     368           68 :         let mut doc = serde_json::json!({
     369           68 :             "id": format!("urn:ngsi-ld:ContextSourceRegistration:529-{}",
     370           68 :                           extra.to_string().len()),
     371           68 :             "type": "ContextSourceRegistration",
     372           68 :             "information": [{"entities": [{"type": "Building"}]}],
     373           68 :             "endpoint": "http://cs.example.org:1026"
     374              :         });
     375           68 :         for (k, v) in extra.as_object().expect("obj") {
     376           68 :             doc[k] = v.clone();
     377           68 :         }
     378           68 :         let body = doc.to_string();
     379           68 :         let req = Request::post("/ngsi-ld/v1/csourceRegistrations")
     380           68 :             .header("Content-Type", "application/json")
     381           68 :             .header("Content-Length", body.len())
     382           68 :             .body(Body::from(body))
     383           68 :             .expect("req");
     384           68 :         app.oneshot(req).await.expect("resp").status()
     385           68 :     }
     386              : 
     387              :     /// Table 5.2.9-1 value spaces: operations limited to the 4.20 names and
     388              :     /// groups; localOnly boolean; contextSourceAlias a non-empty RFC 7230
     389              :     /// pseudonym token; refreshRate an ISO 8601 duration; datasetId URIs or
     390              :     /// @none; scope per the 4.18 grammar; geometries per 4.7; description
     391              :     /// and registrationName non-empty strings.
     392              :     #[tokio::test]
     393            4 :     async fn registration_member_value_spaces() {
     394              :         use serde_json::json;
     395            4 :         let cases: &[(serde_json::Value, StatusCode)] = &[
     396            4 :             (json!({"operations": ["bogusOp"]}), StatusCode::BAD_REQUEST),
     397            4 :             (
     398            4 :                 json!({"operations": ["updateOps", "retrieveEntity"]}),
     399            4 :                 StatusCode::CREATED,
     400            4 :             ),
     401            4 :             (json!({"localOnly": "yes"}), StatusCode::BAD_REQUEST),
     402            4 :             (json!({"localOnly": true}), StatusCode::CREATED),
     403            4 :             (json!({"contextSourceAlias": ""}), StatusCode::BAD_REQUEST),
     404            4 :             (
     405            4 :                 json!({"contextSourceAlias": "has space"}),
     406            4 :                 StatusCode::BAD_REQUEST,
     407            4 :             ),
     408            4 :             (json!({"contextSourceAlias": "cs1"}), StatusCode::CREATED),
     409            4 :             (json!({"refreshRate": "5 minutes"}), StatusCode::BAD_REQUEST),
     410            4 :             (json!({"refreshRate": "PT5M"}), StatusCode::CREATED),
     411            4 :             (
     412            4 :                 json!({"datasetId": ["urn:ds:1", "@none"]}),
     413            4 :                 StatusCode::CREATED,
     414            4 :             ),
     415            4 :             (json!({"datasetId": ["not a uri"]}), StatusCode::BAD_REQUEST),
     416            4 :             (json!({"scope": "9bad"}), StatusCode::BAD_REQUEST),
     417            4 :             (json!({"scope": ["/Madrid", "/A/B_2"]}), StatusCode::CREATED),
     418            4 :             (json!({"description": ""}), StatusCode::BAD_REQUEST),
     419            4 :             (json!({"registrationName": ""}), StatusCode::BAD_REQUEST),
     420            4 :             (json!({"location": 5}), StatusCode::BAD_REQUEST),
     421            4 :             (
     422            4 :                 json!({"location": {"type": "Point", "coordinates": [8, 40]}}),
     423            4 :                 StatusCode::CREATED,
     424            4 :             ),
     425            4 :         ];
     426           68 :         for (extra, want) in cases {
     427           68 :             let got = post_reg(extra.clone()).await;
     428           68 :             assert_eq!(got, *want, "extra={extra}");
     429            4 :         }
     430            4 :     }
     431              : 
     432              :     /// 5.2.9, after Table 5.2.9-1: the Table 5.2.9-2 members "are read-only
     433              :     /// and shall be automatically generated by NGSI-LD implementations. In
     434              :     /// the event that they are provided (in update or create operations)
     435              :     /// NGSI-LD implementations shall ignore them." A tolerant reader that
     436              :     /// stores an unknown member verbatim would let a client dictate its own
     437              :     /// forward history, and every one of these is served back.
     438              :     #[tokio::test]
     439            4 :     async fn table_5_2_9_2_members_are_ignored_on_create_and_update() {
     440              :         use serde_json::json;
     441              :         const READ_ONLY: [&str; 5] = [
     442              :             "timesSent",
     443              :             "timesFailed",
     444              :             "lastSuccess",
     445              :             "lastFailure",
     446              :             "status",
     447              :         ];
     448            4 :         let id = "urn:ngsi-ld:ContextSourceRegistration:529-readonly";
     449            4 :         let st = AppState::new("t5292".into());
     450            4 :         let claimed = json!({
     451            4 :             "timesSent": 9_000,
     452            4 :             "timesFailed": 0,
     453            4 :             "lastSuccess": "2035-01-01T00:00:00Z",
     454            4 :             "lastFailure": "2035-01-01T00:00:00Z",
     455            4 :             "status": "ok",
     456              :         });
     457              : 
     458            4 :         let mut doc = json!({
     459            4 :             "id": id,
     460            4 :             "type": "ContextSourceRegistration",
     461            4 :             "information": [{"entities": [{"type": "Building"}]}],
     462            4 :             "endpoint": "http://cs.example.org:1026",
     463              :         });
     464           20 :         for (k, v) in claimed.as_object().expect("obj") {
     465           20 :             doc[k] = v.clone();
     466           20 :         }
     467            4 :         let body = doc.to_string();
     468            4 :         let res = router(st.clone())
     469            4 :             .oneshot(
     470            4 :                 Request::post("/ngsi-ld/v1/csourceRegistrations")
     471            4 :                     .header("Content-Type", "application/json")
     472            4 :                     .header("Content-Length", body.len())
     473            4 :                     .body(Body::from(body))
     474            4 :                     .expect("req"),
     475            4 :             )
     476            4 :             .await
     477            4 :             .expect("resp");
     478            4 :         assert_eq!(res.status(), StatusCode::CREATED, "create");
     479              : 
     480            8 :         let served = |st: AppState| async move {
     481            8 :             let res = router(st)
     482            8 :                 .oneshot(
     483            8 :                     Request::get(format!("/ngsi-ld/v1/csourceRegistrations/{id}"))
     484            8 :                         .body(Body::empty())
     485            8 :                         .expect("req"),
     486            8 :                 )
     487            8 :                 .await
     488            8 :                 .expect("resp");
     489            8 :             assert_eq!(res.status(), StatusCode::OK, "retrieve");
     490            8 :             let bytes = axum::body::to_bytes(res.into_body(), usize::MAX)
     491            8 :                 .await
     492            8 :                 .expect("body");
     493            8 :             serde_json::from_slice::<serde_json::Value>(&bytes).expect("json")
     494           16 :         };
     495              : 
     496            4 :         let got = served(st.clone()).await;
     497           20 :         for m in READ_ONLY {
     498           20 :             assert!(
     499           20 :                 got.get(m).is_none(),
     500              :                 "create must ignore the read-only member {m}: {got}"
     501              :             );
     502              :         }
     503              : 
     504              :         // 5.9.3 update: the same members, through the merge patch.
     505            4 :         let patch = claimed.to_string();
     506            4 :         let res = router(st.clone())
     507            4 :             .oneshot(
     508            4 :                 Request::patch(format!("/ngsi-ld/v1/csourceRegistrations/{id}"))
     509            4 :                     .header("Content-Type", "application/json")
     510            4 :                     .header("Content-Length", patch.len())
     511            4 :                     .body(Body::from(patch))
     512            4 :                     .expect("req"),
     513            4 :             )
     514            4 :             .await
     515            4 :             .expect("resp");
     516            4 :         assert_eq!(res.status(), StatusCode::NO_CONTENT, "update");
     517              : 
     518            4 :         let got = served(st).await;
     519           20 :         for m in READ_ONLY {
     520           20 :             assert!(
     521           20 :                 got.get(m).is_none(),
     522            4 :                 "update must ignore the read-only member {m}: {got}"
     523            4 :             );
     524            4 :         }
     525            4 :     }
     526              : }
     527              : 
     528              : #[cfg(test)]
     529              : mod clause_5_2_10 {
     530              :     use super::*;
     531              :     use axum::body::Body;
     532              :     use axum::http::{Request, StatusCode};
     533              :     use tower::ServiceExt;
     534              : 
     535           16 :     async fn post_info(info: serde_json::Value) -> StatusCode {
     536           16 :         let app = router(AppState::new("t5210".into()));
     537           16 :         let body = serde_json::json!({
     538           16 :             "id": format!("urn:ngsi-ld:ContextSourceRegistration:5210-{}",
     539           16 :                           info.to_string().len()),
     540           16 :             "type": "ContextSourceRegistration",
     541           16 :             "information": [info],
     542           16 :             "endpoint": "http://cs.example.org:1026"
     543              :         })
     544           16 :         .to_string();
     545           16 :         let req = Request::post("/ngsi-ld/v1/csourceRegistrations")
     546           16 :             .header("Content-Type", "application/json")
     547           16 :             .header("Content-Length", body.len())
     548           16 :             .body(Body::from(body))
     549           16 :             .expect("req");
     550           16 :         app.oneshot(req).await.expect("resp").status()
     551           16 :     }
     552              : 
     553              :     /// Table 5.2.10-1: empty arrays are not allowed for entities,
     554              :     /// propertyNames or relationshipNames; non-empty name lists are fine.
     555              :     #[tokio::test]
     556            4 :     async fn registration_info_empty_arrays_are_rejected() {
     557              :         use serde_json::json;
     558            4 :         assert_eq!(
     559            4 :             post_info(json!({"entities": []})).await,
     560              :             StatusCode::BAD_REQUEST
     561              :         );
     562            4 :         assert_eq!(
     563            4 :             post_info(json!({"propertyNames": []})).await,
     564              :             StatusCode::BAD_REQUEST,
     565              :             "empty propertyNames"
     566              :         );
     567            4 :         assert_eq!(
     568            4 :             post_info(json!({"relationshipNames": []})).await,
     569              :             StatusCode::BAD_REQUEST,
     570              :             "empty relationshipNames"
     571              :         );
     572            4 :         assert_eq!(
     573            4 :             post_info(json!({"entities": [{"type": "Building"}],
     574            4 :                 "propertyNames": ["speed"],
     575            4 :                 "relationshipNames": ["isParked"]}))
     576            4 :             .await,
     577            4 :             StatusCode::CREATED
     578            4 :         );
     579            4 :     }
     580              : }
     581              : 
     582              : #[cfg(test)]
     583              : mod clause_5_2_11 {
     584              :     use super::*;
     585              :     use axum::body::Body;
     586              :     use axum::http::{Request, StatusCode};
     587              :     use tower::ServiceExt;
     588              : 
     589           20 :     async fn post_interval(iv: serde_json::Value) -> StatusCode {
     590           20 :         let app = router(AppState::new("t5211".into()));
     591           20 :         let body = serde_json::json!({
     592           20 :             "id": format!("urn:ngsi-ld:ContextSourceRegistration:5211-{}",
     593           20 :                           iv.to_string().len()),
     594           20 :             "type": "ContextSourceRegistration",
     595           20 :             "information": [{"entities": [{"type": "Building"}]}],
     596           20 :             "endpoint": "http://cs.example.org:1026",
     597           20 :             "observationInterval": iv
     598              :         })
     599           20 :         .to_string();
     600           20 :         let req = Request::post("/ngsi-ld/v1/csourceRegistrations")
     601           20 :             .header("Content-Type", "application/json")
     602           20 :             .header("Content-Length", body.len())
     603           20 :             .body(Body::from(body))
     604           20 :             .expect("req");
     605           20 :         app.oneshot(req).await.expect("resp").status()
     606           20 :     }
     607              : 
     608              :     /// Table 5.2.11-1: startAt is a mandatory DateTime; endAt optional but a
     609              :     /// DateTime when present (absent = open interval).
     610              :     #[tokio::test]
     611            4 :     async fn time_interval_member_rules() {
     612              :         use serde_json::json;
     613            4 :         assert_eq!(
     614            4 :             post_interval(json!({"endAt": "2030-01-01T00:00:00Z"})).await,
     615              :             StatusCode::BAD_REQUEST,
     616              :             "startAt mandatory"
     617              :         );
     618            4 :         assert_eq!(
     619            4 :             post_interval(json!({"startAt": "2020-01-01"})).await,
     620              :             StatusCode::BAD_REQUEST,
     621              :             "a Date is not a DateTime"
     622              :         );
     623            4 :         assert_eq!(
     624            4 :             post_interval(json!({"startAt": "2020-01-01T00:00:00Z",
     625            4 :                 "endAt": "not a date"}))
     626            4 :             .await,
     627              :             StatusCode::BAD_REQUEST
     628              :         );
     629            4 :         assert_eq!(
     630            4 :             post_interval(json!({"startAt": "2020-01-01T00:00:00Z"})).await,
     631              :             StatusCode::CREATED,
     632              :             "open interval"
     633              :         );
     634            4 :         assert_eq!(
     635            4 :             post_interval(json!({"startAt": "2020-01-01T00:00:00Z",
     636            4 :                 "endAt": "2030-01-01T00:00:00Z"}))
     637            4 :             .await,
     638            4 :             StatusCode::CREATED
     639            4 :         );
     640            4 :     }
     641              : }
     642              : 
     643              : #[cfg(test)]
     644              : mod clause_5_2_12 {
     645              :     use super::*;
     646              :     use axum::body::Body;
     647              :     use axum::http::{Request, StatusCode};
     648              :     use tower::ServiceExt;
     649              : 
     650           16 :     async fn post_sub(doc: serde_json::Value) -> StatusCode {
     651           16 :         let app = router(AppState::new("t5212".into()));
     652           16 :         let body = doc.to_string();
     653           16 :         let req = Request::post("/ngsi-ld/v1/subscriptions")
     654           16 :             .header("Content-Type", "application/json")
     655           16 :             .header("Content-Length", body.len())
     656           16 :             .body(Body::from(body))
     657           16 :             .expect("req");
     658           16 :         app.oneshot(req).await.expect("resp").status()
     659           16 :     }
     660              : 
     661              :     /// 5.2.12: "At least one of (a) entities or (b) watchedAttributes shall
     662              :     /// be present, unless the member localOnly is set to true".
     663              :     #[tokio::test]
     664            4 :     async fn local_only_waives_the_selector_requirement() {
     665              :         use serde_json::json;
     666           16 :         let base = |extra: serde_json::Value| {
     667           16 :             let mut d = json!({
     668           16 :                 "id": format!("urn:ngsi-ld:Subscription:5212-{}", extra.to_string().len()),
     669           16 :                 "type": "Subscription",
     670           16 :                 "notification": {"endpoint": {"uri": "http://client.example.org/cb"}}
     671              :             });
     672           16 :             for (k, v) in extra.as_object().expect("obj") {
     673           16 :                 d[k] = v.clone();
     674           16 :             }
     675           16 :             d
     676           16 :         };
     677            4 :         assert_eq!(
     678            4 :             post_sub(base(json!({}))).await,
     679              :             StatusCode::BAD_REQUEST,
     680              :             "no selector and no localOnly"
     681              :         );
     682            4 :         assert_eq!(
     683            4 :             post_sub(base(json!({"localOnly": true}))).await,
     684              :             StatusCode::CREATED,
     685              :             "localOnly=true waives entities/watchedAttributes"
     686              :         );
     687            4 :         assert_eq!(
     688            4 :             post_sub(base(json!({"localOnly": false}))).await,
     689              :             StatusCode::BAD_REQUEST
     690              :         );
     691              :         // the exclusions stay intact
     692            4 :         assert_eq!(
     693            4 :             post_sub(base(json!({"watchedAttributes": ["speed"],
     694            4 :                 "timeInterval": 5})))
     695            4 :             .await,
     696            4 :             StatusCode::BAD_REQUEST
     697            4 :         );
     698            4 :     }
     699              : }
     700              : 
     701              : #[cfg(test)]
     702              : mod clause_5_2_13 {
     703              :     use super::*;
     704              :     use axum::body::Body;
     705              :     use axum::http::{Request, StatusCode};
     706              :     use tower::ServiceExt;
     707              : 
     708           20 :     async fn post_geoq(geoq: serde_json::Value) -> StatusCode {
     709           20 :         let app = router(AppState::new("t5213".into()));
     710           20 :         let body = serde_json::json!({
     711           20 :             "id": format!("urn:ngsi-ld:Subscription:5213-{}", geoq.to_string().len()),
     712           20 :             "type": "Subscription",
     713           20 :             "entities": [{"type": "Vehicle"}],
     714           20 :             "geoQ": geoq,
     715           20 :             "notification": {"endpoint": {"uri": "http://client.example.org/cb"}}
     716              :         })
     717           20 :         .to_string();
     718           20 :         let req = Request::post("/ngsi-ld/v1/subscriptions")
     719           20 :             .header("Content-Type", "application/json")
     720           20 :             .header("Content-Length", body.len())
     721           20 :             .body(Body::from(body))
     722           20 :             .expect("req");
     723           20 :         app.oneshot(req).await.expect("resp").status()
     724           20 :     }
     725              : 
     726              :     /// Table 5.2.13-1: coordinates as JSON Array OR string form, geometry
     727              :     /// from the legal set (no GeometryCollection), georel per 4.10,
     728              :     /// geoproperty optional.
     729              :     #[tokio::test]
     730            4 :     async fn subscription_geoquery_member_rules() {
     731              :         use serde_json::json;
     732            4 :         assert_eq!(
     733            4 :             post_geoq(json!({"georel": "near;maxDistance==2000",
     734            4 :                 "geometry": "Point", "coordinates": [8, 40]}))
     735            4 :             .await,
     736              :             StatusCode::CREATED
     737              :         );
     738            4 :         assert_eq!(
     739            4 :             post_geoq(json!({"georel": "within", "geometry": "Polygon",
     740            4 :                 "coordinates": "[[[0,0],[4,0],[4,4],[0,4],[0,0]]]",
     741            4 :                 "geoproperty": "observationSpace"}))
     742            4 :             .await,
     743              :             StatusCode::CREATED,
     744              :             "string-encoded coordinates (4.7.1) are legal"
     745              :         );
     746            4 :         assert_eq!(
     747            4 :             post_geoq(json!({"georel": "within",
     748            4 :                 "geometry": "GeometryCollection", "coordinates": []}))
     749            4 :             .await,
     750              :             StatusCode::BAD_REQUEST
     751              :         );
     752            4 :         assert_eq!(
     753            4 :             post_geoq(json!({"georel": "touches", "geometry": "Point",
     754            4 :                 "coordinates": [8, 40]}))
     755            4 :             .await,
     756              :             StatusCode::BAD_REQUEST
     757              :         );
     758            4 :         assert_eq!(
     759            4 :             post_geoq(json!({"geometry": "Point", "coordinates": [8, 40]})).await,
     760            4 :             StatusCode::BAD_REQUEST,
     761            4 :             "georel is mandatory"
     762            4 :         );
     763            4 :     }
     764              : }
     765              : 
     766              : #[cfg(test)]
     767              : mod clause_5_2_14 {
     768              :     use super::*;
     769              :     use axum::body::Body;
     770              :     use axum::http::{Request, StatusCode};
     771              :     use tower::ServiceExt;
     772              : 
     773           36 :     async fn post_notif(n: serde_json::Value) -> StatusCode {
     774           36 :         let app = router(AppState::new("t5214".into()));
     775           36 :         let mut notif = serde_json::json!({"endpoint": {"uri": "http://client.example.org/cb"}});
     776           40 :         for (k, v) in n.as_object().expect("obj") {
     777           40 :             notif[k] = v.clone();
     778           40 :         }
     779           36 :         let body = serde_json::json!({
     780           36 :             "id": format!("urn:ngsi-ld:Subscription:5214-{}", n.to_string().len()),
     781           36 :             "type": "Subscription",
     782           36 :             "entities": [{"type": "Vehicle"}],
     783           36 :             "notification": notif
     784              :         })
     785           36 :         .to_string();
     786           36 :         let req = Request::post("/ngsi-ld/v1/subscriptions")
     787           36 :             .header("Content-Type", "application/json")
     788           36 :             .header("Content-Length", body.len())
     789           36 :             .body(Body::from(body))
     790           36 :             .expect("req");
     791           36 :         app.oneshot(req).await.expect("resp").status()
     792           36 :     }
     793              : 
     794              :     /// Table 5.2.14.1-1: join limited to flat/inline/@none, joinLevel a
     795              :     /// positive integer, sysAttrs/showChanges booleans, attributes may not
     796              :     /// name id/type/scope (it is "a synonym for pick, except that id, type,
     797              :     /// scope are not allowed").
     798              :     #[tokio::test]
     799            4 :     async fn notification_params_value_spaces() {
     800              :         use serde_json::json;
     801            4 :         assert_eq!(
     802            4 :             post_notif(json!({"join": "sideways"})).await,
     803              :             StatusCode::BAD_REQUEST
     804              :         );
     805            4 :         assert_eq!(
     806            4 :             post_notif(json!({"join": "inline"})).await,
     807              :             StatusCode::CREATED
     808              :         );
     809            4 :         assert_eq!(
     810            4 :             post_notif(json!({"joinLevel": 0})).await,
     811              :             StatusCode::BAD_REQUEST
     812              :         );
     813            4 :         assert_eq!(
     814            4 :             post_notif(json!({"joinLevel": 1.5})).await,
     815              :             StatusCode::BAD_REQUEST
     816              :         );
     817            4 :         assert_eq!(
     818            4 :             post_notif(json!({"join": "flat", "joinLevel": 2})).await,
     819              :             StatusCode::CREATED
     820              :         );
     821            4 :         assert_eq!(
     822            4 :             post_notif(json!({"sysAttrs": "yes"})).await,
     823              :             StatusCode::BAD_REQUEST
     824              :         );
     825            4 :         assert_eq!(
     826            4 :             post_notif(json!({"showChanges": "yes"})).await,
     827              :             StatusCode::BAD_REQUEST
     828              :         );
     829            4 :         assert_eq!(
     830            4 :             post_notif(json!({"attributes": ["id"]})).await,
     831              :             StatusCode::BAD_REQUEST,
     832              :             "attributes may not name id/type/scope"
     833              :         );
     834            4 :         assert_eq!(
     835            4 :             post_notif(json!({"attributes": ["speed"]})).await,
     836            4 :             StatusCode::CREATED
     837            4 :         );
     838            4 :     }
     839              : }
     840              : 
     841              : #[cfg(test)]
     842              : mod clause_5_2_14_2 {
     843              :     use super::*;
     844              :     use axum::body::Body;
     845              :     use axum::http::{Request, StatusCode};
     846              :     use http_body_util::BodyExt;
     847              :     use tower::ServiceExt;
     848              : 
     849              :     /// Table 5.2.14.2-1: lastFailure/lastNotification/lastSuccess/timesSent
     850              :     /// are output-only — provided ones "shall ignore them" on create, and
     851              :     /// retrieval never echoes fabricated values.
     852              :     #[tokio::test]
     853            4 :     async fn output_only_members_are_ignored_on_input() {
     854            4 :         let st = AppState::new("t52142".into());
     855            4 :         let app = router(st);
     856            4 :         let body = serde_json::json!({
     857            4 :             "id": "urn:ngsi-ld:Subscription:52142",
     858            4 :             "type": "Subscription",
     859            4 :             "entities": [{"type": "Vehicle"}],
     860            4 :             "notification": {
     861            4 :                 "endpoint": {"uri": "http://client.example.org/cb"},
     862            4 :                 "timesSent": 999,
     863            4 :                 "timesFailed": 999,
     864            4 :                 "lastNotification": "1999-01-01T00:00:00Z",
     865            4 :                 "lastSuccess": "1999-01-01T00:00:00Z",
     866            4 :                 "lastFailure": "1999-01-01T00:00:00Z"
     867              :             }
     868              :         })
     869            4 :         .to_string();
     870            4 :         let req = Request::post("/ngsi-ld/v1/subscriptions")
     871            4 :             .header("Content-Type", "application/json")
     872            4 :             .header("Content-Length", body.len())
     873            4 :             .body(Body::from(body))
     874            4 :             .expect("req");
     875            4 :         let resp = app.clone().oneshot(req).await.expect("resp");
     876            4 :         assert_eq!(
     877            4 :             resp.status(),
     878              :             StatusCode::CREATED,
     879              :             "providing them is not an error"
     880              :         );
     881            4 :         let resp = app
     882            4 :             .oneshot(
     883            4 :                 Request::get("/ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:52142")
     884            4 :                     .body(Body::empty())
     885            4 :                     .expect("req"),
     886            4 :             )
     887            4 :             .await
     888            4 :             .expect("resp");
     889            4 :         assert_eq!(resp.status(), StatusCode::OK);
     890            4 :         let bytes = resp.into_body().collect().await.expect("body").to_bytes();
     891            4 :         let doc: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
     892            4 :         let n = &doc["notification"];
     893           20 :         for k in [
     894            4 :             "timesSent",
     895            4 :             "timesFailed",
     896            4 :             "lastNotification",
     897            4 :             "lastSuccess",
     898            4 :             "lastFailure",
     899            4 :         ] {
     900           20 :             assert!(
     901           20 :                 n.get(k).is_none(),
     902            4 :                 "client-fabricated {k} must be ignored, got {}",
     903            4 :                 n[k]
     904            4 :             );
     905            4 :         }
     906            4 :     }
     907              : }
     908              : 
     909              : #[cfg(test)]
     910              : mod clause_5_2_15 {
     911              :     use super::*;
     912              :     use axum::body::Body;
     913              :     use axum::http::{Request, StatusCode};
     914              :     use tower::ServiceExt;
     915              : 
     916           28 :     async fn post_ep(ep: serde_json::Value) -> StatusCode {
     917           28 :         let app = router(AppState::new("t5215".into()));
     918           28 :         let body = serde_json::json!({
     919           28 :             "id": format!("urn:ngsi-ld:Subscription:5215-{}", ep.to_string().len()),
     920           28 :             "type": "Subscription",
     921           28 :             "entities": [{"type": "Vehicle"}],
     922           28 :             "notification": {"endpoint": ep}
     923              :         })
     924           28 :         .to_string();
     925           28 :         let req = Request::post("/ngsi-ld/v1/subscriptions")
     926           28 :             .header("Content-Type", "application/json")
     927           28 :             .header("Content-Length", body.len())
     928           28 :             .body(Body::from(body))
     929           28 :             .expect("req");
     930           28 :         app.oneshot(req).await.expect("resp").status()
     931           28 :     }
     932              : 
     933              :     /// Table 5.2.15-1: uri mandatory, accept value space, cooldown/timeout
     934              :     /// > 0, receiverInfo/notifierInfo as KeyValuePair[] (5.2.22).
     935              :     #[tokio::test]
     936            4 :     async fn endpoint_member_rules() {
     937              :         use serde_json::json;
     938            4 :         let uri = "http://client.example.org/cb";
     939            4 :         assert_eq!(
     940            4 :             post_ep(json!({"accept": "application/json"})).await,
     941              :             StatusCode::BAD_REQUEST,
     942              :             "uri mandatory"
     943              :         );
     944            4 :         assert_eq!(
     945            4 :             post_ep(json!({"uri": uri, "accept": "text/plain"})).await,
     946              :             StatusCode::BAD_REQUEST
     947              :         );
     948            4 :         assert_eq!(
     949            4 :             post_ep(json!({"uri": uri, "cooldown": 0})).await,
     950              :             StatusCode::BAD_REQUEST
     951              :         );
     952            4 :         assert_eq!(
     953            4 :             post_ep(json!({"uri": uri, "timeout": -1})).await,
     954              :             StatusCode::BAD_REQUEST
     955              :         );
     956            4 :         assert_eq!(
     957            4 :             post_ep(json!({"uri": uri,
     958            4 :                 "receiverInfo": [{"key": "Authorization", "value": "Bearer x"}],
     959            4 :                 "cooldown": 500, "timeout": 3000}))
     960            4 :             .await,
     961              :             StatusCode::CREATED
     962              :         );
     963            4 :         assert_eq!(
     964            4 :             post_ep(json!({"uri": uri, "receiverInfo": ["junk"]})).await,
     965              :             StatusCode::BAD_REQUEST,
     966              :             "receiverInfo entries must be {{key, value}} pairs"
     967              :         );
     968            4 :         assert_eq!(
     969            4 :             post_ep(json!({"uri": uri, "notifierInfo": [{"novalue": true}]})).await,
     970            4 :             StatusCode::BAD_REQUEST,
     971            4 :             "notifierInfo entries must be {{key, value}} pairs"
     972            4 :         );
     973            4 :     }
     974              : }
     975              : 
     976              : #[cfg(test)]
     977              : mod clause_5_2_16 {
     978              :     use super::*;
     979              :     use axum::body::Body;
     980              :     use axum::http::{Request, StatusCode};
     981              :     use http_body_util::BodyExt;
     982              :     use tower::ServiceExt;
     983              : 
     984              :     /// Tables 5.2.16-1/5.2.17-1: a partial batch failure answers 207 with
     985              :     /// success = URI array and errors = BatchEntityError[] (entityId +
     986              :     /// RFC 7807 ProblemDetails).
     987              :     #[tokio::test]
     988            4 :     async fn batch_result_and_entity_error_shapes() {
     989            4 :         let app = router(AppState::new("t5216".into()));
     990            4 :         let body = serde_json::json!([
     991            4 :             {"id": "urn:ngsi-ld:V:ok", "type": "Vehicle"},
     992            4 :             {"id": "not a uri", "type": "Vehicle"}
     993              :         ])
     994            4 :         .to_string();
     995            4 :         let req = Request::post("/ngsi-ld/v1/entityOperations/create")
     996            4 :             .header("Content-Type", "application/json")
     997            4 :             .header("Content-Length", body.len())
     998            4 :             .body(Body::from(body))
     999            4 :             .expect("req");
    1000            4 :         let resp = app.oneshot(req).await.expect("resp");
    1001            4 :         assert_eq!(resp.status(), StatusCode::MULTI_STATUS);
    1002            4 :         let bytes = resp.into_body().collect().await.expect("body").to_bytes();
    1003            4 :         let doc: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
    1004            4 :         assert_eq!(doc["success"], serde_json::json!(["urn:ngsi-ld:V:ok"]));
    1005            4 :         let errs = doc["errors"].as_array().expect("errors array");
    1006            4 :         assert_eq!(errs.len(), 1);
    1007            4 :         let e = &errs[0];
    1008            4 :         assert!(e.get("entityId").is_some());
    1009            4 :         let pd = &e["error"];
    1010           12 :         for k in ["type", "title", "status"] {
    1011           12 :             assert!(
    1012           12 :                 pd.get(k).is_some(),
    1013            4 :                 "ProblemDetails member {k} missing: {pd}"
    1014            4 :             );
    1015            4 :         }
    1016            4 :         assert!(
    1017            4 :             pd["type"].as_str().unwrap_or("").contains("errors/"),
    1018            4 :             "error.type is the NGSI-LD error URI"
    1019            4 :         );
    1020            4 :     }
    1021              : }
    1022              : 
    1023              : #[cfg(test)]
    1024              : mod clause_5_2_18 {
    1025              :     use super::*;
    1026              :     use axum::body::Body;
    1027              :     use axum::http::{Request, StatusCode};
    1028              :     use http_body_util::BodyExt;
    1029              :     use tower::ServiceExt;
    1030              : 
    1031              :     /// Tables 5.2.18-1/5.2.19-1: partial attribute update answers 207 with
    1032              :     /// updated = names and notUpdated = {attributeName, reason}[].
    1033              :     #[tokio::test]
    1034            4 :     async fn update_result_and_not_updated_details_shape() {
    1035            4 :         let app = router(AppState::new("t5218".into()));
    1036            4 :         let body = serde_json::json!({"id": "urn:ngsi-ld:V:5218", "type": "Vehicle",
    1037            4 :             "speed": {"type": "Property", "value": 1}})
    1038            4 :         .to_string();
    1039            4 :         let req = Request::post("/ngsi-ld/v1/entities")
    1040            4 :             .header("Content-Type", "application/json")
    1041            4 :             .header("Content-Length", body.len())
    1042            4 :             .body(Body::from(body))
    1043            4 :             .expect("req");
    1044            4 :         assert_eq!(
    1045            4 :             app.clone().oneshot(req).await.expect("r").status(),
    1046              :             StatusCode::CREATED
    1047              :         );
    1048              :         // noOverwrite append: speed exists (skipped), brand is new (applied)
    1049            4 :         let frag = serde_json::json!({
    1050            4 :             "speed": {"type": "Property", "value": 2},
    1051            4 :             "brand": {"type": "Property", "value": "x"}})
    1052            4 :         .to_string();
    1053            4 :         let req =
    1054            4 :             Request::post("/ngsi-ld/v1/entities/urn:ngsi-ld:V:5218/attrs?options=noOverwrite")
    1055            4 :                 .header("Content-Type", "application/json")
    1056            4 :                 .header("Content-Length", frag.len())
    1057            4 :                 .body(Body::from(frag))
    1058            4 :                 .expect("req");
    1059            4 :         let resp = app.oneshot(req).await.expect("resp");
    1060            4 :         assert_eq!(resp.status(), StatusCode::MULTI_STATUS);
    1061            4 :         let bytes = resp.into_body().collect().await.expect("body").to_bytes();
    1062            4 :         let doc: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
    1063            4 :         let updated = doc["updated"].as_array().expect("updated");
    1064            4 :         assert!(
    1065            4 :             updated
    1066            4 :                 .iter()
    1067            4 :                 .any(|u| u.as_str().unwrap_or("").contains("brand")),
    1068              :             "brand applied: {doc}"
    1069              :         );
    1070            4 :         let nu = doc["notUpdated"].as_array().expect("notUpdated");
    1071            4 :         assert_eq!(nu.len(), 1, "{doc}");
    1072            4 :         assert!(nu[0]["attributeName"]
    1073            4 :             .as_str()
    1074            4 :             .unwrap_or("")
    1075            4 :             .contains("speed"));
    1076            4 :         assert!(!nu[0]["reason"].as_str().unwrap_or("").is_empty());
    1077            4 :         assert!(
    1078            4 :             nu[0].get("registrationId").is_none(),
    1079            4 :             "local failure carries no registrationId"
    1080            4 :         );
    1081            4 :     }
    1082              : }
    1083              : 
    1084              : #[cfg(test)]
    1085              : mod clause_5_2_21 {
    1086              :     use super::*;
    1087              :     use axum::body::Body;
    1088              :     use axum::http::{Request, StatusCode};
    1089              :     use http_body_util::BodyExt;
    1090              :     use serde_json::json;
    1091              :     use tower::ServiceExt;
    1092              : 
    1093           24 :     async fn temporal_query(qs: &str) -> StatusCode {
    1094           24 :         let app = router(AppState::new("t5221".into()));
    1095           24 :         let req = Request::get(format!(
    1096              :             "/ngsi-ld/v1/temporal/entities?type=Vehicle&timerel=after&timeAt=2020-01-01T00:00:00Z{qs}"
    1097              :         ))
    1098           24 :         .body(Body::empty())
    1099           24 :         .expect("req");
    1100           24 :         app.oneshot(req).await.expect("resp").status()
    1101           24 :     }
    1102              : 
    1103              :     /// Table 5.2.21-1: lastN is a POSITIVE integer; aggrMethods entries are
    1104              :     /// limited to the 4.5.19 methods; endTimeAt is mandatory for between.
    1105              :     #[tokio::test]
    1106            4 :     async fn temporal_query_member_value_spaces() {
    1107            4 :         assert_eq!(
    1108            4 :             temporal_query("&lastN=0").await,
    1109              :             StatusCode::BAD_REQUEST,
    1110              :             "lastN=0"
    1111              :         );
    1112            4 :         assert_eq!(temporal_query("&lastN=-3").await, StatusCode::BAD_REQUEST);
    1113            4 :         assert_eq!(temporal_query("&lastN=5").await, StatusCode::OK);
    1114            4 :         assert_eq!(
    1115            4 :             temporal_query("&aggrMethods=bogus").await,
    1116              :             StatusCode::BAD_REQUEST
    1117              :         );
    1118            4 :         assert_eq!(
    1119            4 :             temporal_query("&options=aggregatedValues").await,
    1120              :             StatusCode::BAD_REQUEST,
    1121              :             "aggregatedValues without aggrMethods"
    1122              :         );
    1123            4 :         assert_eq!(
    1124            4 :             temporal_query("&options=aggregatedValues&aggrMethods=avg,max").await,
    1125              :             StatusCode::OK
    1126              :         );
    1127            4 :         let app = router(AppState::new("t5221b".into()));
    1128            4 :         let req = Request::get(
    1129              :             "/ngsi-ld/v1/temporal/entities?type=V&timerel=between&timeAt=2020-01-01T00:00:00Z",
    1130              :         )
    1131            4 :         .body(Body::empty())
    1132            4 :         .expect("req");
    1133            4 :         assert_eq!(
    1134            4 :             app.oneshot(req).await.expect("resp").status(),
    1135            4 :             StatusCode::BAD_REQUEST,
    1136            4 :             "between without endTimeAt"
    1137            4 :         );
    1138            4 :     }
    1139              : 
    1140              :     /// POST Query (5.2.23) carrying a temporalQ object (5.2.21 JSON form).
    1141           56 :     async fn post_tq(tq: serde_json::Value, qs: &str) -> (StatusCode, String) {
    1142           56 :         let app = router(AppState::new("t5221j".into()));
    1143           56 :         let body = json!({
    1144           56 :             "type": "Query",
    1145           56 :             "entities": [{"type": "Vehicle"}],
    1146           56 :             "temporalQ": tq
    1147              :         })
    1148           56 :         .to_string();
    1149           56 :         let req = Request::post(format!("/ngsi-ld/v1/temporal/entityOperations/query{qs}"))
    1150           56 :             .header("Content-Type", "application/json")
    1151           56 :             .header("Content-Length", body.len())
    1152           56 :             .body(Body::from(body))
    1153           56 :             .expect("req");
    1154           56 :         let resp = app.oneshot(req).await.expect("resp");
    1155           56 :         let status = resp.status();
    1156           56 :         let bytes = resp.into_body().collect().await.expect("body").to_bytes();
    1157           56 :         (status, String::from_utf8_lossy(&bytes).into_owned())
    1158           56 :     }
    1159              : 
    1160              :     /// Table 5.2.21-1 (JSON form): lastN is a positive INTEGER — zero,
    1161              :     /// negative, fractional and string values are all outside the value
    1162              :     /// space; timerel is limited to before/after/between; endTimeAt is
    1163              :     /// mandatory for between; timeproperty is limited to the four 4.8 names.
    1164              :     #[tokio::test]
    1165            4 :     async fn temporal_query_json_member_value_spaces() {
    1166            4 :         let ok = json!({"timerel": "after", "timeAt": "2020-01-01T00:00:00Z"});
    1167           36 :         let with = |k: &str, v: serde_json::Value| {
    1168           36 :             let mut t = ok.clone();
    1169           36 :             t[k] = v;
    1170           36 :             t
    1171           36 :         };
    1172            4 :         let (st, body) = post_tq(with("lastN", json!(5)), "").await;
    1173            4 :         assert_eq!(st, StatusCode::OK, "{body}");
    1174            4 :         assert!(!body.contains("BadRequestData"), "{body}");
    1175           16 :         for bad in [json!(0), json!(-3), json!(2.5), json!("5")] {
    1176           16 :             let (st, body) = post_tq(with("lastN", bad.clone()), "").await;
    1177           16 :             assert_eq!(st, StatusCode::BAD_REQUEST, "lastN={bad}");
    1178           16 :             assert!(body.contains("lastN"), "{body}");
    1179              :         }
    1180            4 :         let (st, _) = post_tq(with("timerel", json!("bogus")), "").await;
    1181            4 :         assert_eq!(st, StatusCode::BAD_REQUEST);
    1182            4 :         let (st, _) = post_tq(with("timerel", json!("between")), "").await;
    1183            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "between without endTimeAt");
    1184            4 :         let (st, _) = post_tq(with("timeproperty", json!("expiresAt")), "").await;
    1185            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "timeproperty outside 4.8 set");
    1186            4 :         let (st, _) = post_tq(with("timeAt", json!(20200101)), "").await;
    1187            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "timeAt must be a string");
    1188            4 :     }
    1189              : 
    1190              :     /// Table 5.2.21-1: aggrMethods (comma separated list of string — both
    1191              :     /// the string and string-array spellings) and aggrPeriodDuration are
    1192              :     /// carried by the JSON TemporalQuery and honoured when
    1193              :     /// aggregatedValues is requested via format/options.
    1194              :     #[tokio::test]
    1195            4 :     async fn temporal_query_json_aggregation_members() {
    1196            4 :         let base = json!({"timerel": "after", "timeAt": "2020-01-01T00:00:00Z"});
    1197           16 :         let with = |k: &str, v: serde_json::Value| {
    1198           16 :             let mut t = base.clone();
    1199           16 :             t[k] = v;
    1200           16 :             t
    1201           16 :         };
    1202            4 :         let (st, body) = post_tq(
    1203            4 :             with("aggrMethods", json!(["avg", "max"])),
    1204            4 :             "?format=aggregatedValues",
    1205              :         )
    1206            4 :         .await;
    1207            4 :         assert_eq!(st, StatusCode::OK, "array aggrMethods honoured: {body}");
    1208            4 :         let (st, body) = post_tq(
    1209            4 :             with("aggrMethods", json!("avg,max")),
    1210            4 :             "?format=aggregatedValues",
    1211              :         )
    1212            4 :         .await;
    1213            4 :         assert_eq!(st, StatusCode::OK, "string aggrMethods honoured: {body}");
    1214            4 :         let (st, body) = post_tq(
    1215            4 :             with("aggrMethods", json!(["bogus"])),
    1216            4 :             "?format=aggregatedValues",
    1217              :         )
    1218            4 :         .await;
    1219            4 :         assert_eq!(st, StatusCode::BAD_REQUEST);
    1220            4 :         assert!(body.contains("aggrMethods"), "{body}");
    1221            4 :         let (st, _) = post_tq(with("aggrMethods", json!(42)), "?format=aggregatedValues").await;
    1222            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "aggrMethods wrong JSON type");
    1223            4 :         let mut t = base.clone();
    1224            4 :         t["aggrMethods"] = json!(["avg"]);
    1225            4 :         t["aggrPeriodDuration"] = json!("bogus");
    1226            4 :         let (st, body) = post_tq(t, "?format=aggregatedValues").await;
    1227            4 :         assert_eq!(st, StatusCode::BAD_REQUEST);
    1228            4 :         assert!(body.contains("aggrPeriodDuration"), "{body}");
    1229            4 :     }
    1230              : 
    1231              :     /// 5.2.21 used from Subscription.temporalQ (5.2.12, CSR subscriptions):
    1232              :     /// timerel and timeAt are cardinality 1 — a temporalQ violating the
    1233              :     /// data type is rejected at subscription creation.
    1234              :     #[tokio::test]
    1235            4 :     async fn subscription_temporal_q_validated() {
    1236           20 :         async fn post_sub(tq: serde_json::Value) -> StatusCode {
    1237           20 :             let app = router(AppState::new("t5221s".into()));
    1238           20 :             let body = json!({
    1239           20 :                 "id": format!("urn:ngsi-ld:Subscription:5221-{}", tq.to_string().len()),
    1240           20 :                 "type": "Subscription",
    1241           20 :                 "entities": [{"type": "Building"}],
    1242           20 :                 "notification": {"endpoint": {"uri": "http://client.example.org/cb"}},
    1243           20 :                 "temporalQ": tq
    1244              :             })
    1245           20 :             .to_string();
    1246           20 :             let req = Request::post("/ngsi-ld/v1/subscriptions")
    1247           20 :                 .header("Content-Type", "application/json")
    1248           20 :                 .header("Content-Length", body.len())
    1249           20 :                 .body(Body::from(body))
    1250           20 :                 .expect("req");
    1251           20 :             app.oneshot(req).await.expect("resp").status()
    1252           20 :         }
    1253            4 :         assert_eq!(
    1254            4 :             post_sub(json!({
    1255            4 :                 "timerel": "after",
    1256            4 :                 "timeAt": "2020-06-01T22:07:00Z",
    1257            4 :                 "timeproperty": "createdAt"
    1258            4 :             }))
    1259            4 :             .await,
    1260              :             StatusCode::CREATED,
    1261              :             "official fixture shape stays creatable"
    1262              :         );
    1263            4 :         assert_eq!(
    1264            4 :             post_sub(json!({"timerel": "after"})).await,
    1265              :             StatusCode::BAD_REQUEST,
    1266              :             "timeAt is cardinality 1"
    1267              :         );
    1268            4 :         assert_eq!(
    1269            4 :             post_sub(json!({"timeAt": "2020-06-01T22:07:00Z"})).await,
    1270              :             StatusCode::BAD_REQUEST,
    1271              :             "timerel is cardinality 1"
    1272              :         );
    1273            4 :         assert_eq!(
    1274            4 :             post_sub(json!({"timerel": "bogus", "timeAt": "2020-06-01T22:07:00Z"})).await,
    1275              :             StatusCode::BAD_REQUEST
    1276              :         );
    1277            4 :         assert_eq!(
    1278            4 :             post_sub(json!("after")).await,
    1279            4 :             StatusCode::BAD_REQUEST,
    1280            4 :             "temporalQ must be an object"
    1281            4 :         );
    1282            4 :     }
    1283              : }
    1284              : 
    1285              : #[cfg(test)]
    1286              : mod clause_5_2_22 {
    1287              :     use super::*;
    1288              :     use axum::body::Body;
    1289              :     use axum::http::{Request, StatusCode};
    1290              :     use serde_json::json;
    1291              :     use tower::ServiceExt;
    1292              : 
    1293           52 :     async fn post(path: &str, doc: serde_json::Value, tenant: &str) -> StatusCode {
    1294           52 :         let app = router(AppState::new(tenant.into()));
    1295           52 :         let body = doc.to_string();
    1296           52 :         let req = Request::post(format!("/ngsi-ld/v1/{path}"))
    1297           52 :             .header("Content-Type", "application/json")
    1298           52 :             .header("Content-Length", body.len())
    1299           52 :             .body(Body::from(body))
    1300           52 :             .expect("req");
    1301           52 :         app.oneshot(req).await.expect("resp").status()
    1302           52 :     }
    1303              : 
    1304              :     /// Table 5.2.22-1: key AND value are Strings, both cardinality 1 —
    1305              :     /// enforced on the notification endpoint's receiverInfo/notifierInfo.
    1306              :     #[tokio::test]
    1307            4 :     async fn endpoint_info_values_must_be_strings() {
    1308           28 :         let sub = |info_key: &str, entries: serde_json::Value| {
    1309           28 :             let mut d = json!({
    1310           28 :                 "type": "Subscription",
    1311           28 :                 "entities": [{"type": "Building"}],
    1312           28 :                 "notification": {"endpoint": {"uri": "http://client.example.org/cb"}}
    1313              :             });
    1314           28 :             d["notification"]["endpoint"][info_key] = entries;
    1315           28 :             d
    1316           28 :         };
    1317            4 :         assert_eq!(
    1318            4 :             post(
    1319            4 :                 "subscriptions",
    1320            4 :                 sub(
    1321            4 :                     "receiverInfo",
    1322            4 :                     json!([{"key": "Authorization", "value": "Bearer x"}])
    1323            4 :                 ),
    1324            4 :                 "t5222a"
    1325            4 :             )
    1326            4 :             .await,
    1327              :             StatusCode::CREATED,
    1328              :             "string values stay creatable"
    1329              :         );
    1330           20 :         for bad in [
    1331            4 :             json!(42),
    1332            4 :             json!({"a": 1}),
    1333            4 :             json!(["x"]),
    1334            4 :             json!(null),
    1335            4 :             json!(true),
    1336            4 :         ] {
    1337           20 :             assert_eq!(
    1338           20 :                 post(
    1339           20 :                     "subscriptions",
    1340           20 :                     sub("receiverInfo", json!([{"key": "K", "value": bad}])),
    1341           20 :                     "t5222a"
    1342           20 :                 )
    1343           20 :                 .await,
    1344            4 :                 StatusCode::BAD_REQUEST,
    1345            4 :                 "receiverInfo value {bad} is not a String"
    1346            4 :             );
    1347            4 :         }
    1348            4 :         assert_eq!(
    1349            4 :             post(
    1350            4 :                 "subscriptions",
    1351            4 :                 sub("notifierInfo", json!([{"key": "K", "value": 7}])),
    1352            4 :                 "t5222a"
    1353            4 :             )
    1354            4 :             .await,
    1355            4 :             StatusCode::BAD_REQUEST,
    1356            4 :             "notifierInfo value must be a String"
    1357            4 :         );
    1358            4 :     }
    1359              : 
    1360              :     /// Table 5.2.22-1 via 5.2.9 contextSourceInfo: every pair's value is a
    1361              :     /// String — a non-string value on a custom key is rejected at
    1362              :     /// registration, not at first forward.
    1363              :     #[tokio::test]
    1364            4 :     async fn context_source_info_values_must_be_strings() {
    1365           24 :         let csr = |info: serde_json::Value| {
    1366           24 :             json!({
    1367           24 :                 "type": "ContextSourceRegistration",
    1368           24 :                 "endpoint": "http://peer.example/ngsi-ld/v1",
    1369           24 :                 "information": [{"entities": [{"type": "Building"}]}],
    1370           24 :                 "contextSourceInfo": info
    1371              :             })
    1372           24 :         };
    1373            4 :         assert_eq!(
    1374            4 :             post(
    1375            4 :                 "csourceRegistrations",
    1376            4 :                 csr(json!([{"key": "X-Auth-Token", "value": "abc"}])),
    1377            4 :                 "t5222b"
    1378            4 :             )
    1379            4 :             .await,
    1380              :             StatusCode::CREATED,
    1381              :             "string values stay registrable"
    1382              :         );
    1383           16 :         for bad in [json!(123), json!(["a"]), json!({"v": 1}), json!(null)] {
    1384           16 :             assert_eq!(
    1385           16 :                 post(
    1386           16 :                     "csourceRegistrations",
    1387           16 :                     csr(json!([{"key": "X-Custom", "value": bad}])),
    1388           16 :                     "t5222b"
    1389           16 :                 )
    1390           16 :                 .await,
    1391            4 :                 StatusCode::BAD_REQUEST,
    1392            4 :                 "contextSourceInfo value {bad} is not a String"
    1393            4 :             );
    1394            4 :         }
    1395            4 :         assert_eq!(
    1396            4 :             post(
    1397            4 :                 "csourceRegistrations",
    1398            4 :                 csr(json!([{"key": 5, "value": "v"}])),
    1399            4 :                 "t5222b"
    1400            4 :             )
    1401            4 :             .await,
    1402            4 :             StatusCode::BAD_REQUEST,
    1403            4 :             "key must be a String"
    1404            4 :         );
    1405            4 :     }
    1406              : }
    1407              : 
    1408              : #[cfg(test)]
    1409              : mod clause_5_2_44 {
    1410              :     use super::*;
    1411              :     use axum::body::Body;
    1412              :     use axum::http::{Request, StatusCode};
    1413              :     use http_body_util::BodyExt;
    1414              :     use serde_json::json;
    1415              :     use tower::ServiceExt;
    1416              : 
    1417              :     /// Table 5.2.44-1: aggrParams carries aggrMethods (comma separated list
    1418              :     /// of strings — string and string-array spellings) + aggrPeriodDuration,
    1419              :     /// honoured on the temporal operation when aggregatedValues is requested.
    1420              :     #[tokio::test]
    1421            4 :     async fn aggregation_params_members() {
    1422            4 :         let app = router(AppState::new("t5244".into()));
    1423           24 :         let send = |doc: serde_json::Value, qs: &'static str| {
    1424           24 :             let app = app.clone();
    1425           24 :             async move {
    1426           24 :                 let body = doc.to_string();
    1427           24 :                 let req = Request::post(format!("/ngsi-ld/v1/temporal/entityOperations/query{qs}"))
    1428           24 :                     .header("Content-Type", "application/json")
    1429           24 :                     .header("Content-Length", body.len())
    1430           24 :                     .body(Body::from(body))
    1431           24 :                     .expect("req");
    1432           24 :                 let resp = app.oneshot(req).await.expect("resp");
    1433           24 :                 let status = resp.status();
    1434           24 :                 let bytes = resp.into_body().collect().await.expect("body").to_bytes();
    1435           24 :                 (status, String::from_utf8_lossy(&bytes).into_owned())
    1436           24 :             }
    1437           24 :         };
    1438           24 :         let with_ap = |ap: serde_json::Value| {
    1439           24 :             json!({"type": "Query", "entities": [{"type": "Vehicle"}],
    1440           24 :                 "temporalQ": {"timerel": "after", "timeAt": "2020-01-01T00:00:00Z"},
    1441           24 :                 "aggrParams": ap})
    1442           24 :         };
    1443            4 :         let (st, body) = send(
    1444            4 :             with_ap(json!({"aggrMethods": ["avg"], "aggrPeriodDuration": "PT1H"})),
    1445            4 :             "?format=aggregatedValues",
    1446            4 :         )
    1447            4 :         .await;
    1448            4 :         assert_eq!(st, StatusCode::OK, "array spelling honoured: {body}");
    1449            4 :         assert!(!body.contains("BadRequestData"), "{body}");
    1450            4 :         let (st, _) = send(
    1451            4 :             with_ap(json!({"aggrMethods": "avg,max"})),
    1452            4 :             "?format=aggregatedValues",
    1453            4 :         )
    1454            4 :         .await;
    1455            4 :         assert_eq!(st, StatusCode::OK, "string spelling honoured");
    1456            4 :         let (st, body) = send(
    1457            4 :             with_ap(json!({"aggrMethods": ["bogus"]})),
    1458            4 :             "?format=aggregatedValues",
    1459            4 :         )
    1460            4 :         .await;
    1461            4 :         assert_eq!(st, StatusCode::BAD_REQUEST);
    1462            4 :         assert!(body.contains("aggrMethods"), "{body}");
    1463            4 :         let (st, body) = send(
    1464            4 :             with_ap(json!({"aggrMethods": ["avg"], "aggrPeriodDuration": "bogus"})),
    1465            4 :             "?format=aggregatedValues",
    1466            4 :         )
    1467            4 :         .await;
    1468            4 :         assert_eq!(st, StatusCode::BAD_REQUEST);
    1469            4 :         assert!(body.contains("aggrPeriodDuration"), "{body}");
    1470            4 :         let (st, _) = send(
    1471            4 :             with_ap(json!({"aggrMethods": 42})),
    1472            4 :             "?format=aggregatedValues",
    1473            4 :         )
    1474            4 :         .await;
    1475            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "aggrMethods wrong JSON type");
    1476            4 :         let (st, _) = send(with_ap(json!("avg")), "?format=aggregatedValues").await;
    1477            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "aggrParams must be an object");
    1478            4 :     }
    1479              : }
    1480              : 
    1481              : #[cfg(test)]
    1482              : mod clause_5_2_43 {
    1483              :     use super::*;
    1484              :     use axum::body::Body;
    1485              :     use axum::http::{Request, StatusCode};
    1486              :     use http_body_util::BodyExt;
    1487              :     use serde_json::json;
    1488              :     use tower::ServiceExt;
    1489              : 
    1490           36 :     async fn send(
    1491           36 :         app: &axum::Router,
    1492           36 :         method: &str,
    1493           36 :         path: &str,
    1494           36 :         doc: Option<serde_json::Value>,
    1495           36 :     ) -> (StatusCode, String) {
    1496           36 :         let mut b = Request::builder()
    1497           36 :             .method(method)
    1498           36 :             .uri(format!("/ngsi-ld/v1/{path}"));
    1499           36 :         let body = match doc {
    1500           32 :             Some(d) => {
    1501           32 :                 let s = d.to_string();
    1502           32 :                 b = b
    1503           32 :                     .header("Content-Type", "application/json")
    1504           32 :                     .header("Content-Length", s.len());
    1505           32 :                 Body::from(s)
    1506              :             }
    1507            4 :             None => Body::empty(),
    1508              :         };
    1509           36 :         let resp = app
    1510           36 :             .clone()
    1511           36 :             .oneshot(b.body(body).expect("req"))
    1512           36 :             .await
    1513           36 :             .expect("resp");
    1514           36 :         let status = resp.status();
    1515           36 :         let bytes = resp.into_body().collect().await.expect("body").to_bytes();
    1516           36 :         (status, String::from_utf8_lossy(&bytes).into_owned())
    1517           36 :     }
    1518              : 
    1519              :     /// Table 5.2.43-1: orderBy maps to the 4.23 keys, coordinates (JSON
    1520              :     /// array, mandatory for dist ordering) + geometry (default Point) form
    1521              :     /// the reference geometry; collation is rejected loudly while only
    1522              :     /// codepoint order is offered (4.23.1 named gap).
    1523              :     #[tokio::test]
    1524            4 :     async fn ordering_params_members() {
    1525            4 :         let app = router(AppState::new("t5243".into()));
    1526            8 :         for (id, lon, lat) in [
    1527            4 :             ("urn:ngsi-ld:Vehicle:near", 8.01, 40.01),
    1528            4 :             ("urn:ngsi-ld:Vehicle:far", 10.0, 45.0),
    1529            4 :         ] {
    1530            8 :             let (st, body) = send(
    1531            8 :                 &app,
    1532            8 :                 "POST",
    1533            8 :                 "entities",
    1534            8 :                 Some(json!({"id": id, "type": "Vehicle",
    1535            8 :                     "location": {"type": "GeoProperty",
    1536            8 :                         "value": {"type": "Point", "coordinates": [lon, lat]}}})),
    1537              :             )
    1538            8 :             .await;
    1539            8 :             assert_eq!(st, StatusCode::CREATED, "{body}");
    1540              :         }
    1541            4 :         let q = "entityOperations/query";
    1542           24 :         let with_ordering = |o: serde_json::Value| json!({"type": "Query", "entities": [{"type": "Vehicle"}], "ordering": o});
    1543            4 :         let (st, body) = send(
    1544            4 :             &app,
    1545            4 :             "POST",
    1546            4 :             q,
    1547            4 :             Some(with_ordering(json!({"orderBy": ["location;dist-asc"],
    1548            4 :                 "coordinates": [8, 40], "geometry": "Point"}))),
    1549              :         )
    1550            4 :         .await;
    1551            4 :         assert_eq!(st, StatusCode::OK, "{body}");
    1552            4 :         let near = body.find("urn:ngsi-ld:Vehicle:near").expect("near in body");
    1553            4 :         let far = body.find("urn:ngsi-ld:Vehicle:far").expect("far in body");
    1554            4 :         assert!(near < far, "dist-asc must order near before far: {body}");
    1555            4 :         let (st, _) = send(
    1556            4 :             &app,
    1557            4 :             "POST",
    1558            4 :             q,
    1559            4 :             Some(with_ordering(json!({"orderBy": ["location;dist-asc"]}))),
    1560              :         )
    1561            4 :         .await;
    1562            4 :         assert_eq!(
    1563              :             st,
    1564              :             StatusCode::BAD_REQUEST,
    1565              :             "dist ordering without coordinates"
    1566              :         );
    1567            4 :         let (st, _) = send(
    1568            4 :             &app,
    1569            4 :             "POST",
    1570            4 :             q,
    1571            4 :             Some(with_ordering(json!({"orderBy": ["location;dist-asc"],
    1572            4 :                 "coordinates": "8,40"}))),
    1573              :         )
    1574            4 :         .await;
    1575            4 :         assert_eq!(
    1576              :             st,
    1577              :             StatusCode::BAD_REQUEST,
    1578              :             "coordinates must be a JSON array"
    1579              :         );
    1580            4 :         let (st, _) = send(
    1581            4 :             &app,
    1582            4 :             "POST",
    1583            4 :             q,
    1584            4 :             Some(with_ordering(json!({"orderBy": ["id;asc"], "geometry": 5}))),
    1585              :         )
    1586            4 :         .await;
    1587            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "geometry must be a string");
    1588              :         // 4.23.3 EXAMPLE 7: a named ICU collation is honoured (200), and a
    1589              :         // non-string member stays a loud 400
    1590            4 :         let (st, body) = send(
    1591            4 :             &app,
    1592            4 :             "POST",
    1593            4 :             q,
    1594            4 :             Some(with_ordering(json!({"orderBy": ["id;asc"],
    1595            4 :                 "collation": "de-u-co-phonebk"}))),
    1596              :         )
    1597            4 :         .await;
    1598            4 :         assert_eq!(st, StatusCode::OK, "named collation is honoured: {body}");
    1599            4 :         let (st, body) = send(
    1600            4 :             &app,
    1601            4 :             "POST",
    1602            4 :             q,
    1603            4 :             Some(with_ordering(
    1604            4 :                 json!({"orderBy": ["id;asc"], "collation": 5}),
    1605            4 :             )),
    1606              :         )
    1607            4 :         .await;
    1608            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "{body}");
    1609            4 :         assert!(body.contains("collation"), "{body}");
    1610              :         // GET twin: orderGeometry is a legal query parameter
    1611            4 :         let (st, body) = send(
    1612            4 :             &app,
    1613            4 :             "GET",
    1614            4 :             "entities?type=Vehicle&orderBy=location;dist-asc&orderFrom=[8,40]&orderGeometry=Point",
    1615            4 :             None,
    1616              :         )
    1617            4 :         .await;
    1618            4 :         assert_eq!(st, StatusCode::OK, "{body}");
    1619            4 :     }
    1620              : }
    1621              : 
    1622              : #[cfg(test)]
    1623              : mod clause_5_2_34 {
    1624              :     use super::*;
    1625              :     use axum::body::Body;
    1626              :     use axum::http::{Request, StatusCode};
    1627              :     use serde_json::json;
    1628              :     use tower::ServiceExt;
    1629              : 
    1630           32 :     async fn post_csr(management: serde_json::Value) -> StatusCode {
    1631           32 :         let app = router(AppState::new("t5234".into()));
    1632           32 :         let body = json!({
    1633           32 :             "type": "ContextSourceRegistration",
    1634           32 :             "endpoint": "http://peer.example/ngsi-ld/v1",
    1635           32 :             "information": [{"entities": [{"type": "Building"}]}],
    1636           32 :             "management": management
    1637              :         })
    1638           32 :         .to_string();
    1639           32 :         let req = Request::post("/ngsi-ld/v1/csourceRegistrations")
    1640           32 :             .header("Content-Type", "application/json")
    1641           32 :             .header("Content-Length", body.len())
    1642           32 :             .body(Body::from(body))
    1643           32 :             .expect("req");
    1644           32 :         app.oneshot(req).await.expect("resp").status()
    1645           32 :     }
    1646              : 
    1647              :     /// Table 5.2.34-1: cacheDuration is an ISO 8601 duration, cooldown and
    1648              :     /// timeout are numbers greater than 0, localOnly is a boolean, and the
    1649              :     /// member itself is an object.
    1650              :     #[tokio::test]
    1651            4 :     async fn registration_management_info_value_spaces() {
    1652            4 :         assert_eq!(
    1653            4 :             post_csr(json!({"cacheDuration": "PT5M", "cooldown": 500,
    1654            4 :                 "timeout": 3000, "localOnly": true}))
    1655            4 :             .await,
    1656              :             StatusCode::CREATED,
    1657              :             "conformant management info stays registrable"
    1658              :         );
    1659           28 :         for (label, m) in [
    1660            4 :             ("non-object", json!("yes")),
    1661            4 :             ("bad cacheDuration", json!({"cacheDuration": "bogus"})),
    1662            4 :             ("cacheDuration wrong type", json!({"cacheDuration": 300})),
    1663            4 :             ("cooldown zero", json!({"cooldown": 0})),
    1664            4 :             ("timeout negative", json!({"timeout": -5})),
    1665            4 :             ("timeout wrong type", json!({"timeout": "3000"})),
    1666            4 :             ("localOnly wrong type", json!({"localOnly": "yes"})),
    1667            4 :         ] {
    1668           28 :             assert_eq!(
    1669           28 :                 post_csr(m).await,
    1670            4 :                 StatusCode::BAD_REQUEST,
    1671            4 :                 "management {label}"
    1672            4 :             );
    1673            4 :         }
    1674            4 :     }
    1675              : }
    1676              : 
    1677              : #[cfg(test)]
    1678              : mod clause_5_2_33 {
    1679              :     use super::*;
    1680              :     use axum::body::Body;
    1681              :     use axum::http::{Request, StatusCode};
    1682              :     use http_body_util::BodyExt;
    1683              :     use serde_json::json;
    1684              :     use tower::ServiceExt;
    1685              : 
    1686           40 :     async fn send(app: &axum::Router, path: &str, doc: &serde_json::Value) -> (StatusCode, String) {
    1687           40 :         let body = doc.to_string();
    1688           40 :         let req = Request::post(format!("/ngsi-ld/v1/{path}"))
    1689           40 :             .header("Content-Type", "application/json")
    1690           40 :             .header("Content-Length", body.len())
    1691           40 :             .body(Body::from(body))
    1692           40 :             .expect("req");
    1693           40 :         let resp = app.clone().oneshot(req).await.expect("resp");
    1694           40 :         let status = resp.status();
    1695           40 :         let bytes = resp.into_body().collect().await.expect("body").to_bytes();
    1696           40 :         (status, String::from_utf8_lossy(&bytes).into_owned())
    1697           40 :     }
    1698              : 
    1699              :     /// Table 5.2.33-1: id is "String or String[]" of valid URIs, type is
    1700              :     /// mandatory, and "id takes precedence over idPattern".
    1701              :     #[tokio::test]
    1702            4 :     async fn entity_selector_id_forms_and_precedence() {
    1703            4 :         let app = router(AppState::new("t5233a".into()));
    1704           12 :         for id in [
    1705            4 :             "urn:ngsi-ld:Vehicle:A1",
    1706            4 :             "urn:ngsi-ld:Vehicle:A2",
    1707            4 :             "urn:ngsi-ld:Vehicle:B1",
    1708            4 :         ] {
    1709           12 :             let (st, body) = send(
    1710           12 :                 &app,
    1711           12 :                 "entities",
    1712           12 :                 &json!({"id": id, "type": "Vehicle",
    1713           12 :                         "speed": {"type": "Property", "value": 1}}),
    1714              :             )
    1715           12 :             .await;
    1716           12 :             assert_eq!(st, StatusCode::CREATED, "{body}");
    1717              :         }
    1718            4 :         let q = "entityOperations/query";
    1719           20 :         let sel = |e: serde_json::Value| json!({"type": "Query", "entities": [e]});
    1720            4 :         let (st, body) = send(
    1721            4 :             &app,
    1722            4 :             q,
    1723            4 :             &sel(json!({"type": "Vehicle",
    1724            4 :                 "id": ["urn:ngsi-ld:Vehicle:A1", "urn:ngsi-ld:Vehicle:A2"]})),
    1725              :         )
    1726            4 :         .await;
    1727            4 :         assert_eq!(st, StatusCode::OK, "id array form: {body}");
    1728            4 :         assert!(body.contains("urn:ngsi-ld:Vehicle:A1"), "{body}");
    1729            4 :         assert!(body.contains("urn:ngsi-ld:Vehicle:A2"), "{body}");
    1730            4 :         assert!(!body.contains("urn:ngsi-ld:Vehicle:B1"), "{body}");
    1731            4 :         let (st, body) = send(
    1732            4 :             &app,
    1733            4 :             q,
    1734            4 :             &sel(json!({"type": "Vehicle", "id": "urn:ngsi-ld:Vehicle:A1",
    1735            4 :                 "idPattern": "^urn:ngsi-ld:Vehicle:B.*$"})),
    1736              :         )
    1737            4 :         .await;
    1738            4 :         assert_eq!(st, StatusCode::OK, "{body}");
    1739            4 :         assert!(
    1740            4 :             body.contains("urn:ngsi-ld:Vehicle:A1"),
    1741              :             "id takes precedence over idPattern: {body}"
    1742              :         );
    1743            4 :         let (st, _) = send(&app, q, &sel(json!({"id": "urn:ngsi-ld:Vehicle:A1"}))).await;
    1744            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "type is mandatory (5.2.33)");
    1745            4 :         let (st, _) = send(
    1746            4 :             &app,
    1747            4 :             q,
    1748            4 :             &sel(json!({"type": "Vehicle", "id": ["urn:ngsi-ld:Vehicle:A1", 5]})),
    1749              :         )
    1750            4 :         .await;
    1751            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "id entries must be strings");
    1752            4 :         let (st, _) = send(&app, q, &sel(json!({"type": "Vehicle", "id": "not a uri"}))).await;
    1753            4 :         assert_eq!(st, StatusCode::BAD_REQUEST, "id must be a valid URI");
    1754            4 :     }
    1755              : 
    1756              :     /// 5.2.33 in Subscription.entities: the String[] id form is accepted at
    1757              :     /// creation.
    1758              :     #[tokio::test]
    1759            4 :     async fn subscription_selector_id_array() {
    1760            4 :         let app = router(AppState::new("t5233b".into()));
    1761            8 :         let sub = |e: serde_json::Value| {
    1762            8 :             json!({"type": "Subscription", "entities": [e],
    1763            8 :                 "notification": {"endpoint": {"uri": "http://client.example.org/cb"}}})
    1764            8 :         };
    1765            4 :         let (st, body) = send(
    1766            4 :             &app,
    1767            4 :             "subscriptions",
    1768            4 :             &sub(json!({"type": "Building",
    1769            4 :                 "id": ["urn:ngsi-ld:Building:a", "urn:ngsi-ld:Building:b"]})),
    1770              :         )
    1771            4 :         .await;
    1772            4 :         assert_eq!(st, StatusCode::CREATED, "{body}");
    1773            4 :         let (st, _) = send(
    1774            4 :             &app,
    1775            4 :             "subscriptions",
    1776            4 :             &sub(json!({"type": "Building", "id": ["urn:ngsi-ld:Building:a", 5]})),
    1777              :         )
    1778            4 :         .await;
    1779            4 :         assert_eq!(st, StatusCode::BAD_REQUEST);
    1780            4 :     }
    1781              : }
    1782              : 
    1783              : #[cfg(test)]
    1784              : mod clause_5_2_23 {
    1785              :     use super::*;
    1786              :     use axum::body::Body;
    1787              :     use axum::http::{Request, StatusCode};
    1788              :     use http_body_util::BodyExt;
    1789              :     use serde_json::json;
    1790              :     use tower::ServiceExt;
    1791              : 
    1792          116 :     async fn send(
    1793          116 :         app: &axum::Router,
    1794          116 :         method: &str,
    1795          116 :         path: &str,
    1796          116 :         doc: &serde_json::Value,
    1797          116 :     ) -> (StatusCode, String) {
    1798          116 :         let body = doc.to_string();
    1799          116 :         let req = Request::builder()
    1800          116 :             .method(method)
    1801          116 :             .uri(format!("/ngsi-ld/v1/{path}"))
    1802          116 :             .header("Content-Type", "application/json")
    1803          116 :             .header("Content-Length", body.len())
    1804          116 :             .body(Body::from(body))
    1805          116 :             .expect("req");
    1806          116 :         let resp = app.clone().oneshot(req).await.expect("resp");
    1807          116 :         let status = resp.status();
    1808          116 :         let bytes = resp.into_body().collect().await.expect("body").to_bytes();
    1809          116 :         (status, String::from_utf8_lossy(&bytes).into_owned())
    1810          116 :     }
    1811              : 
    1812           80 :     fn query(extra: serde_json::Value) -> serde_json::Value {
    1813           80 :         let mut d = json!({"type": "Query", "entities": [{"type": "Vehicle"}]});
    1814           84 :         for (k, v) in extra.as_object().expect("obj") {
    1815           84 :             d[k] = v.clone();
    1816           84 :         }
    1817           80 :         d
    1818           80 :     }
    1819              : 
    1820              :     /// Table 5.2.23-1: member value spaces — empty arrays are not allowed
    1821              :     /// (entities/attrs/pick/omit), string members must be strings, joinLevel
    1822              :     /// is a positive integer, geoQ/ordering are objects, and
    1823              :     /// temporalQ/aggrParams are only allowed on the temporal operation.
    1824              :     #[tokio::test]
    1825            4 :     async fn query_body_member_value_spaces() {
    1826            4 :         let app = router(AppState::new("t5223a".into()));
    1827            4 :         let q = "entityOperations/query";
    1828            4 :         let (st, body) = send(&app, "POST", q, &query(json!({}))).await;
    1829            4 :         assert_eq!(st, StatusCode::OK, "control: {body}");
    1830            4 :         assert!(!body.contains("BadRequestData"), "{body}");
    1831           72 :         for (label, doc) in [
    1832            4 :             ("type not Query", json!({"type": "NotQuery"})),
    1833            4 :             ("entities empty", json!({"type": "Query", "entities": []})),
    1834            4 :             (
    1835            4 :                 "entities not array",
    1836            4 :                 query(json!({"entities": "Vehicle"})).clone(),
    1837            4 :             ),
    1838            4 :             ("attrs empty", query(json!({"attrs": []}))),
    1839            4 :             ("attrs non-string entry", query(json!({"attrs": [7]}))),
    1840            4 :             ("pick empty", query(json!({"pick": []}))),
    1841            4 :             ("omit empty", query(json!({"omit": []}))),
    1842            4 :             ("q not a string", query(json!({"q": 42}))),
    1843            4 :             ("csf not a string", query(json!({"csf": 42}))),
    1844            4 :             (
    1845            4 :                 "datasetId not an array",
    1846            4 :                 query(json!({"datasetId": "urn:x"})),
    1847            4 :             ),
    1848            4 :             (
    1849            4 :                 "joinLevel zero",
    1850            4 :                 query(json!({"join": "inline", "joinLevel": 0})),
    1851            4 :             ),
    1852            4 :             (
    1853            4 :                 "joinLevel fractional",
    1854            4 :                 query(json!({"join": "inline", "joinLevel": 2.5})),
    1855            4 :             ),
    1856            4 :             ("geoQ not an object", query(json!({"geoQ": "near"}))),
    1857            4 :             ("ordering not an object", query(json!({"ordering": "asc"}))),
    1858            4 :             (
    1859            4 :                 "splitEntities not a boolean",
    1860            4 :                 query(json!({"splitEntities": "yes"})),
    1861            4 :             ),
    1862            4 :             (
    1863            4 :                 "entityMap not a boolean",
    1864            4 :                 query(json!({"entityMap": "yes"})),
    1865            4 :             ),
    1866            4 :             (
    1867            4 :                 "temporalQ only for the temporal operation",
    1868            4 :                 query(json!({"temporalQ": {"timerel": "after", "timeAt": "2020-01-01T00:00:00Z"}})),
    1869            4 :             ),
    1870            4 :             (
    1871            4 :                 "aggrParams only for the temporal operation",
    1872            4 :                 query(json!({"aggrParams": {"aggrMethods": "avg"}})),
    1873            4 :             ),
    1874            4 :         ] {
    1875           72 :             let (st, body) = send(&app, "POST", q, &doc).await;
    1876           72 :             assert_eq!(st, StatusCode::BAD_REQUEST, "{label}: {body}");
    1877              :         }
    1878              :         // entities selector: non-object entries violate 5.2.33 EntitySelector[]
    1879            4 :         let (st, _) = send(
    1880            4 :             &app,
    1881            4 :             "POST",
    1882            4 :             q,
    1883            4 :             &json!({"type": "Query", "entities": ["Vehicle"]}),
    1884              :         )
    1885            4 :         .await;
    1886            4 :         assert_eq!(
    1887            4 :             st,
    1888            4 :             StatusCode::BAD_REQUEST,
    1889            4 :             "entities entries must be objects"
    1890            4 :         );
    1891            4 :     }
    1892              : 
    1893              :     /// Table 5.2.23-1: q/pick/omit conveyed in the body are honoured exactly
    1894              :     /// like their 6.3.7 query-parameter twins.
    1895              :     #[tokio::test]
    1896            4 :     async fn query_body_members_are_honoured() {
    1897            4 :         let app = router(AppState::new("t5223b".into()));
    1898            8 :         for (id, speed) in [
    1899            4 :             ("urn:ngsi-ld:Vehicle:A1", 80),
    1900            4 :             ("urn:ngsi-ld:Vehicle:A2", 120),
    1901            4 :         ] {
    1902            8 :             let (st, body) = send(
    1903            8 :                 &app,
    1904            8 :                 "POST",
    1905            8 :                 "entities",
    1906            8 :                 &json!({"id": id, "type": "Vehicle",
    1907            8 :                         "speed": {"type": "Property", "value": speed},
    1908            8 :                         "brand": {"type": "Property", "value": "Mercedes"}}),
    1909              :             )
    1910            8 :             .await;
    1911            8 :             assert_eq!(st, StatusCode::CREATED, "{body}");
    1912              :         }
    1913            4 :         let q = "entityOperations/query";
    1914            4 :         let (st, body) = send(&app, "POST", q, &query(json!({"q": "speed>100"}))).await;
    1915            4 :         assert_eq!(st, StatusCode::OK, "{body}");
    1916            4 :         assert!(body.contains("urn:ngsi-ld:Vehicle:A2"), "{body}");
    1917            4 :         assert!(
    1918            4 :             !body.contains("urn:ngsi-ld:Vehicle:A1"),
    1919              :             "q must filter: {body}"
    1920              :         );
    1921            4 :         let (st, body) = send(&app, "POST", q, &query(json!({"pick": ["speed"]}))).await;
    1922            4 :         assert_eq!(st, StatusCode::OK, "{body}");
    1923            4 :         assert!(body.contains("speed"), "{body}");
    1924            4 :         assert!(!body.contains("brand"), "pick must project: {body}");
    1925            4 :         let (st, body) = send(&app, "POST", q, &query(json!({"omit": ["speed"]}))).await;
    1926            4 :         assert_eq!(st, StatusCode::OK, "{body}");
    1927            4 :         assert!(body.contains("brand"), "{body}");
    1928            4 :         assert!(!body.contains("speed"), "omit must remove: {body}");
    1929            4 :     }
    1930              : 
    1931              :     /// Table 5.2.23-1 on the temporal operation (5.7.4): the entities
    1932              :     /// selector's id member selects, temporalQ is accepted, and containedBy
    1933              :     /// is "Only applicable for the Retrieve Entity and Query Entities
    1934              :     /// operations".
    1935              :     #[tokio::test]
    1936            4 :     async fn temporal_query_body_entity_selector() {
    1937            4 :         let app = router(AppState::new("t5223c".into()));
    1938            8 :         for id in ["urn:ngsi-ld:Vehicle:T1", "urn:ngsi-ld:Vehicle:T2"] {
    1939            8 :             let (st, body) = send(
    1940            8 :                 &app,
    1941            8 :                 "POST",
    1942            8 :                 "temporal/entities",
    1943            8 :                 &json!({"id": id, "type": "Vehicle",
    1944            8 :                         "speed": [{"type": "Property", "value": 1,
    1945            8 :                                    "observedAt": "2020-08-01T12:00:00Z"}]}),
    1946              :             )
    1947            8 :             .await;
    1948            8 :             assert!(st.is_success(), "{st} {body}");
    1949              :         }
    1950            4 :         let tq = json!({"timerel": "after", "timeAt": "2020-01-01T00:00:00Z"});
    1951            4 :         let doc = json!({"type": "Query",
    1952            4 :             "entities": [{"type": "Vehicle", "id": "urn:ngsi-ld:Vehicle:T1"}],
    1953            4 :             "temporalQ": tq});
    1954            4 :         let (st, body) = send(&app, "POST", "temporal/entityOperations/query", &doc).await;
    1955            4 :         assert_eq!(st, StatusCode::OK, "{body}");
    1956            4 :         assert!(body.contains("urn:ngsi-ld:Vehicle:T1"), "{body}");
    1957            4 :         assert!(
    1958            4 :             !body.contains("urn:ngsi-ld:Vehicle:T2"),
    1959              :             "entities id selector must narrow the temporal query: {body}"
    1960              :         );
    1961            4 :         let doc = json!({"type": "Query", "entities": [{"type": "Vehicle"}],
    1962            4 :             "temporalQ": tq, "containedBy": ["urn:ngsi-ld:Vehicle:T2"]});
    1963            4 :         let (st, _) = send(&app, "POST", "temporal/entityOperations/query", &doc).await;
    1964            4 :         assert_eq!(
    1965            4 :             st,
    1966            4 :             StatusCode::BAD_REQUEST,
    1967            4 :             "containedBy is not applicable to 5.7.4"
    1968            4 :         );
    1969            4 :     }
    1970              : }
    1971              : 
    1972              : #[cfg(test)]
    1973              : mod clause_6_3_11 {
    1974              :     use super::*;
    1975              :     use axum::body::Body;
    1976              :     use axum::http::{Request, StatusCode};
    1977              :     use axum::response::Response;
    1978              :     use http_body_util::BodyExt;
    1979              :     use tower::ServiceExt;
    1980              : 
    1981           16 :     async fn body_json(resp: Response) -> serde_json::Value {
    1982           16 :         let bytes = resp.into_body().collect().await.expect("body").to_bytes();
    1983           16 :         serde_json::from_slice(&bytes).expect("json body")
    1984           16 :     }
    1985              : 
    1986              :     /// 6.3.11 Table 6.3.11-1: `expiresAt` is included in response payloads
    1987              :     /// only when `options=sysAttrs` — entity level, attribute level and
    1988              :     /// temporal instances alike (same gate as createdAt/modifiedAt).
    1989              :     #[tokio::test]
    1990            4 :     async fn clause_6_3_11_expires_at_gated_by_sysattrs() {
    1991            4 :         let mut st = AppState::new("antares-test".into());
    1992            4 :         crate::wire(&mut st).await;
    1993            4 :         let app = router(st);
    1994            4 :         let entity = serde_json::json!({
    1995            4 :             "id": "urn:ngsi-ld:Building:exp6311", "type": "Building",
    1996            4 :             "expiresAt": "2100-01-01T00:00:00Z",
    1997            4 :             "name": {"type": "Property", "value": "x",
    1998            4 :                      "observedAt": "2026-08-13T00:00:00Z",
    1999            4 :                      "expiresAt": "2100-01-01T00:00:00Z"}
    2000              :         });
    2001            4 :         let body = entity.to_string();
    2002            4 :         let resp = app
    2003            4 :             .clone()
    2004            4 :             .oneshot(
    2005            4 :                 Request::post("/ngsi-ld/v1/entities")
    2006            4 :                     .header("Content-Type", "application/json")
    2007            4 :                     .header("Content-Length", body.len())
    2008            4 :                     .body(Body::from(body))
    2009            4 :                     .expect("req"),
    2010            4 :             )
    2011            4 :             .await
    2012            4 :             .expect("resp");
    2013            4 :         assert_eq!(resp.status(), StatusCode::CREATED);
    2014              : 
    2015            4 :         let plain = body_json(
    2016            4 :             app.clone()
    2017            4 :                 .oneshot(
    2018            4 :                     Request::get("/ngsi-ld/v1/entities/urn:ngsi-ld:Building:exp6311")
    2019            4 :                         .body(Body::empty())
    2020            4 :                         .expect("req"),
    2021            4 :                 )
    2022            4 :                 .await
    2023            4 :                 .expect("resp"),
    2024              :         )
    2025            4 :         .await;
    2026            4 :         assert!(
    2027            4 :             plain.get("expiresAt").is_none(),
    2028              :             "entity expiresAt must be sysAttrs-gated (6.3.11): {plain}"
    2029              :         );
    2030            4 :         assert!(
    2031            4 :             plain["name"].get("expiresAt").is_none(),
    2032              :             "attribute expiresAt must be sysAttrs-gated (6.3.11): {plain}"
    2033              :         );
    2034              : 
    2035            4 :         let sys = body_json(
    2036            4 :             app.clone()
    2037            4 :                 .oneshot(
    2038            4 :                     Request::get(
    2039            4 :                         "/ngsi-ld/v1/entities/urn:ngsi-ld:Building:exp6311?options=sysAttrs",
    2040            4 :                     )
    2041            4 :                     .body(Body::empty())
    2042            4 :                     .expect("req"),
    2043            4 :                 )
    2044            4 :                 .await
    2045            4 :                 .expect("resp"),
    2046              :         )
    2047            4 :         .await;
    2048            4 :         assert_eq!(sys["expiresAt"], "2100-01-01T00:00:00Z");
    2049            4 :         assert_eq!(sys["name"]["expiresAt"], "2100-01-01T00:00:00Z");
    2050              : 
    2051            8 :         let inst_of = |body: &serde_json::Value| -> serde_json::Value {
    2052            8 :             let a = &body["name"];
    2053            8 :             if a.is_array() {
    2054            8 :                 a[0].clone()
    2055              :             } else {
    2056            0 :                 a.clone()
    2057              :             }
    2058            8 :         };
    2059            4 :         let t_plain = body_json(
    2060            4 :             app.clone()
    2061            4 :                 .oneshot(
    2062            4 :                     Request::get(
    2063            4 :                         "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Building:exp6311?timerel=after&timeAt=2020-01-01T00:00:00Z",
    2064            4 :                     )
    2065            4 :                     .body(Body::empty())
    2066            4 :                     .expect("req"),
    2067            4 :                 )
    2068            4 :                 .await
    2069            4 :                 .expect("resp"),
    2070            4 :         ).await
    2071              :         ;
    2072            4 :         assert!(
    2073            4 :             inst_of(&t_plain).get("expiresAt").is_none(),
    2074              :             "temporal instance expiresAt must be sysAttrs-gated (6.3.11): {t_plain}"
    2075              :         );
    2076            4 :         let t_sys = body_json(
    2077            4 :             app.clone()
    2078            4 :                 .oneshot(
    2079            4 :                     Request::get(
    2080            4 :                         "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Building:exp6311?timerel=after&timeAt=2020-01-01T00:00:00Z&options=sysAttrs",
    2081            4 :                     )
    2082            4 :                     .body(Body::empty())
    2083            4 :                     .expect("req"),
    2084            4 :                 )
    2085            4 :                 .await
    2086            4 :                 .expect("resp"),
    2087            4 :         ).await
    2088              :         ;
    2089            4 :         assert_eq!(inst_of(&t_sys)["expiresAt"], "2100-01-01T00:00:00Z");
    2090            4 :     }
    2091              : }
    2092              : 
    2093              : /// The programmatic egress override, not `ANTARES_EGRESS_ALLOW_PRIVATE`: a
    2094              : /// sibling test reading the environment while another rewrote it saw the
    2095              : /// policy missing and refused the loopback forward. An atomic store carries
    2096              : /// the same switch with no write for a reader to land in the middle of.
    2097              : #[cfg(test)]
    2098          152 : pub(crate) fn allow_private() {
    2099          152 :     antares_jsonld::allow_private_egress(true);
    2100          152 : }
    2101              : 
    2102              : #[cfg(test)]
    2103              : mod f_ant2_019_background_count {
    2104              :     /// A request-born task that panics is still uncounted when it ends: 64
    2105              :     /// of them left the count 64 up for good, and a shutdown drain waited
    2106              :     /// its whole deadline for them. Other tests' tasks are few and short.
    2107              :     #[tokio::test(flavor = "multi_thread")]
    2108            4 :     async fn a_panicked_task_leaves_the_count() {
    2109              :         const N: usize = 64;
    2110            4 :         let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(N);
    2111            4 :         for _ in 0..N {
    2112          256 :             let tx = tx.clone();
    2113          256 :             super::spawn(async move {
    2114          256 :                 drop(tx);
    2115          256 :                 std::panic::resume_unwind(Box::new("background task fails"));
    2116              :             });
    2117              :         }
    2118            4 :         drop(tx);
    2119              :         // every sender gone: every task ran to its panic
    2120            4 :         assert!(rx.recv().await.is_none());
    2121            4 :         tokio::time::sleep(std::time::Duration::from_millis(50)).await;
    2122            4 :         assert!(
    2123            4 :             super::background_tasks() < N,
    2124            4 :             "{}",
    2125            4 :             super::background_tasks()
    2126            4 :         );
    2127            4 :     }
    2128              : }
        

Generated by: LCOV version 2.0-1