LCOV - code coverage report
Current view: top level - antares-api/src - surface.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 100.0 % 44 44
Test Date: 2026-09-21 10:31:06 Functions: 70.0 % 10 7

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! Extra HTTP surfaces, registered rather than hard-wired.
       3              : //!
       4              : //! CIM 009 6.2 owns `/ngsi-ld/v1`; everything a deployment serves beside it
       5              : //! lives under a reserved prefix, so an added surface can never shadow,
       6              : //! extend or contradict a spec resource. `/q` is this broker's operational
       7              : //! ground and belongs to the admin surface; `/x` and anything under it is
       8              : //! free for a deployment. `/ex` is neither: it is the broker's own
       9              : //! peer-facing wire (the 5.8.1.4 notification receiver, ADR-0019), a core
      10              : //! route no surface can claim, since a prefix outside `/q` and `/x` is
      11              : //! refused below.
      12              : 
      13              : use crate::AppState;
      14              : use axum::Router;
      15              : use serde_json::Value;
      16              : 
      17              : /// One mountable group of routes outside the NGSI-LD API root.
      18              : pub trait ApiSurface: Send + Sync {
      19              :     /// The name `ANTARES_API_SURFACES` selects it by, and the key it
      20              :     /// appears under in `/q/health`.
      21              :     fn name(&self) -> &str;
      22              : 
      23              :     /// Where the routes mount: `/q`, or `/x` and anything below it. The
      24              :     /// routes the surface returns are relative to this.
      25              :     fn prefix(&self) -> &str;
      26              : 
      27              :     /// The routes, relative to `prefix`. The broker supplies the state.
      28              :     fn router(&self, st: AppState) -> Router<AppState>;
      29              : 
      30              :     /// What `/q/health` reports about it, beside its prefix.
      31              :     fn version_info(&self) -> Value;
      32              : }
      33              : 
      34              : /// The prefixes a surface may mount under. `/q` is the broker's operational
      35              : /// ground; `/x` is the deployment's.
      36              : const RESERVED: [&str; 2] = ["/q", "/x"];
      37              : 
      38              : /// A prefix a surface is allowed to claim: exactly one of the reserved
      39              : /// roots, or a path below `/x`. Everything else — the NGSI-LD API root
      40              : /// above all — is refused, since a surface that could mount there would
      41              : /// make conformance a function of deployment configuration.
      42          168 : pub(crate) fn check_prefix(prefix: &str) -> Result<(), String> {
      43          168 :     if RESERVED.contains(&prefix) || prefix.starts_with("/x/") {
      44           88 :         return Ok(());
      45           80 :     }
      46           80 :     Err(format!(
      47           80 :         "api surface prefix {prefix:?} is not reserved; a surface mounts at /q, at /x, \
      48           80 :          or below /x — never under the NGSI-LD API root"
      49           80 :     ))
      50          168 : }
      51              : 
      52              : /// Two prefixes claim the same ground when they are equal or one nests
      53              : /// inside the other. Merging both would leave the winner to route-matching
      54              : /// order, so it is refused where it can still be a startup error.
      55           84 : pub(crate) fn overlaps(a: &str, b: &str) -> bool {
      56           84 :     a == b || a.starts_with(&format!("{b}/")) || b.starts_with(&format!("{a}/"))
      57           84 : }
      58              : 
      59              : #[cfg(test)]
      60              : mod tests {
      61              :     use super::*;
      62              : 
      63              :     #[test]
      64            4 :     fn only_the_reserved_prefixes_are_mountable() {
      65           16 :         for ok in ["/q", "/x", "/x/plugin", "/x/a/b"] {
      66           16 :             assert!(check_prefix(ok).is_ok(), "{ok}");
      67              :         }
      68           56 :         for bad in [
      69            4 :             "/ngsi-ld",
      70            4 :             "/ngsi-ld/v1",
      71            4 :             "/ngsi-ld/v1/entities",
      72            4 :             "/",
      73            4 :             "",
      74            4 :             "/entities",
      75            4 :             "/qq",
      76            4 :             "/q/health",
      77            4 :             "x",
      78            4 :             "q",
      79            4 :             "//x",
      80            4 :             // the peer-facing wire of 5.8.1.4 is a core route, so a surface
      81            4 :             // cannot claim it or anything under it
      82            4 :             "/ex",
      83            4 :             "/ex/v1",
      84            4 :             "/ex/v1/remote-notify",
      85            4 :         ] {
      86           56 :             let err = check_prefix(bad).expect_err(bad);
      87           56 :             assert!(err.contains(bad), "the message names the prefix: {err}");
      88              :         }
      89            4 :     }
      90              : 
      91              :     #[test]
      92            4 :     fn a_prefix_collides_with_itself_and_with_its_ancestors() {
      93            4 :         assert!(overlaps("/x", "/x"));
      94            4 :         assert!(overlaps("/x", "/x/deeper"));
      95            4 :         assert!(overlaps("/x/deeper", "/x"));
      96            4 :         assert!(!overlaps("/q", "/x"));
      97            4 :         assert!(!overlaps("/x/a", "/x/b"));
      98              :         // a shared text prefix is not a shared path segment
      99            4 :         assert!(!overlaps("/x/ab", "/x/abc"));
     100            4 :     }
     101              : }
        

Generated by: LCOV version 2.0-1