mirror of https://github.com/fafhrd91/actix-web
Compare commits
2 Commits
895969923c
...
b75fcd8fac
| Author | SHA1 | Date |
|---|---|---|
|
|
b75fcd8fac | |
|
|
68f01edb72 |
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Add `experimental-introspection` feature for retrieving configured route paths and HTTP methods.
|
||||
- Add `experimental-introspection` feature to report configured routes (paths, methods, guards, resource metadata), include reachability hints for shadowed/conflicting routes, and provide a separate report for external resources.
|
||||
- Minimum supported Rust version (MSRV) is now 1.82.
|
||||
|
||||
## 4.12.1
|
||||
|
|
|
|||
|
|
@ -41,6 +41,16 @@ async fn main() -> std::io::Result<()> {
|
|||
.body(report)
|
||||
}
|
||||
|
||||
// GET /introspection/externals for external resources report
|
||||
async fn introspection_handler_externals(
|
||||
tree: web::Data<actix_web::introspection::IntrospectionTree>,
|
||||
) -> impl Responder {
|
||||
let report = tree.report_externals_as_json();
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(report)
|
||||
}
|
||||
|
||||
// GET /introspection for plain text response
|
||||
async fn introspection_handler_text(
|
||||
tree: web::Data<actix_web::introspection::IntrospectionTree>,
|
||||
|
|
@ -95,6 +105,21 @@ async fn main() -> std::io::Result<()> {
|
|||
HttpResponse::Ok().body("Welcome to the Root Endpoint!")
|
||||
}
|
||||
|
||||
// GET /alpha and /beta (named multi-pattern resource)
|
||||
async fn multi_pattern() -> impl Responder {
|
||||
HttpResponse::Ok().body("Hello from multi-pattern resource!")
|
||||
}
|
||||
|
||||
// GET /acceptable (Acceptable guard)
|
||||
async fn acceptable_guarded() -> impl Responder {
|
||||
HttpResponse::Ok().body("Acceptable guard matched!")
|
||||
}
|
||||
|
||||
// GET /hosted (Host guard)
|
||||
async fn host_guarded() -> impl Responder {
|
||||
HttpResponse::Ok().body("Host guard matched!")
|
||||
}
|
||||
|
||||
// Additional endpoints for /extra
|
||||
fn extra_endpoints(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
|
|
@ -169,6 +194,8 @@ async fn main() -> std::io::Result<()> {
|
|||
// Get introspection report
|
||||
// curl --location '127.0.0.1:8080/introspection' --header 'Accept: application/json'
|
||||
// curl --location '127.0.0.1:8080/introspection' --header 'Accept: text/plain'
|
||||
// curl --location '127.0.0.1:8080/introspection/externals'
|
||||
.external_resource("app-external", "https://example.com/{id}")
|
||||
.service(
|
||||
web::resource("/introspection")
|
||||
.route(
|
||||
|
|
@ -182,9 +209,31 @@ async fn main() -> std::io::Result<()> {
|
|||
.to(introspection_handler_text),
|
||||
),
|
||||
)
|
||||
.service(
|
||||
web::resource("/introspection/externals")
|
||||
.route(web::get().to(introspection_handler_externals)),
|
||||
)
|
||||
.service(
|
||||
web::resource(["/alpha", "/beta"])
|
||||
.name("multi")
|
||||
.route(web::get().to(multi_pattern)),
|
||||
)
|
||||
.route(
|
||||
"/acceptable",
|
||||
web::get()
|
||||
.guard(guard::Acceptable::new(mime::APPLICATION_JSON).match_star_star())
|
||||
.to(acceptable_guarded),
|
||||
)
|
||||
.route(
|
||||
"/hosted",
|
||||
web::get().guard(guard::Host("127.0.0.1")).to(host_guarded),
|
||||
)
|
||||
// API endpoints under /api
|
||||
.service(
|
||||
web::scope("/api")
|
||||
.configure(|cfg| {
|
||||
cfg.external_resource("api-external", "https://api.example/{id}");
|
||||
})
|
||||
// Endpoints under /api/v1
|
||||
.service(
|
||||
web::scope("/v1")
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ where
|
|||
|
||||
// external resources
|
||||
for mut rdef in mem::take(&mut *self.external.borrow_mut()) {
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
self.introspector.borrow_mut().register_external(&rdef, "/");
|
||||
}
|
||||
rmap.add(&mut rdef, None);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ pub struct AppService {
|
|||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) introspector:
|
||||
std::rc::Rc<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) scope_id_stack: Vec<usize>,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pending_scope_id: Option<usize>,
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
|
|
@ -51,6 +55,10 @@ impl AppService {
|
|||
introspector: std::rc::Rc::new(std::cell::RefCell::new(
|
||||
crate::introspection::IntrospectionCollector::new(),
|
||||
)),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
scope_id_stack: Vec::new(),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pending_scope_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +112,10 @@ impl AppService {
|
|||
current_prefix: self.current_prefix.clone(),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: std::rc::Rc::clone(&self.introspector),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
scope_id_stack: self.scope_id_stack.clone(),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pending_scope_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,19 +150,6 @@ impl AppService {
|
|||
{
|
||||
use std::borrow::Borrow;
|
||||
|
||||
// Build the full path for introspection
|
||||
let pat = rdef.pattern().unwrap_or("").to_string();
|
||||
|
||||
let full_path = if self.current_prefix.is_empty() {
|
||||
pat.clone()
|
||||
} else {
|
||||
format!(
|
||||
"{}/{}",
|
||||
self.current_prefix.trim_end_matches('/'),
|
||||
pat.trim_start_matches('/')
|
||||
)
|
||||
};
|
||||
|
||||
// Extract methods and guards for introspection
|
||||
let guard_list: &[Box<dyn Guard>] = guards.borrow().as_ref().map_or(&[], |v| &v[..]);
|
||||
let methods = guard_list
|
||||
|
|
@ -170,15 +169,40 @@ impl AppService {
|
|||
.iter()
|
||||
.map(|g| g.name().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let guard_details = crate::introspection::guard_reports_from_iter(guard_list.iter());
|
||||
|
||||
// Determine if the registered service is a resource
|
||||
let is_resource = rdef.pattern().is_some();
|
||||
self.introspector.borrow_mut().register_pattern_detail(
|
||||
full_path,
|
||||
methods,
|
||||
guard_names,
|
||||
is_resource,
|
||||
);
|
||||
let is_resource = nested.is_none();
|
||||
let full_paths = crate::introspection::expand_patterns(&self.current_prefix, &rdef);
|
||||
let patterns = rdef
|
||||
.pattern_iter()
|
||||
.map(|pattern| pattern.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let resource_name = rdef.name().map(|name| name.to_string());
|
||||
let is_prefix = rdef.is_prefix();
|
||||
let scope_id = if nested.is_some() {
|
||||
self.pending_scope_id.take()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let parent_scope_id = self.scope_id_stack.last().copied();
|
||||
|
||||
for full_path in full_paths {
|
||||
let info = crate::introspection::RouteInfo::new(
|
||||
full_path,
|
||||
methods.clone(),
|
||||
guard_names.clone(),
|
||||
guard_details.clone(),
|
||||
patterns.clone(),
|
||||
resource_name.clone(),
|
||||
);
|
||||
self.introspector.borrow_mut().register_service(
|
||||
info,
|
||||
is_resource,
|
||||
is_prefix,
|
||||
scope_id,
|
||||
parent_scope_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.services
|
||||
|
|
@ -188,15 +212,23 @@ impl AppService {
|
|||
/// Update the current path prefix.
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) fn update_prefix(&mut self, prefix: &str) {
|
||||
self.current_prefix = if self.current_prefix.is_empty() {
|
||||
prefix.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}/{}",
|
||||
self.current_prefix.trim_end_matches('/'),
|
||||
prefix.trim_start_matches('/')
|
||||
)
|
||||
};
|
||||
let next = ResourceDef::root_prefix(prefix);
|
||||
|
||||
if self.current_prefix.is_empty() {
|
||||
self.current_prefix = next.pattern().unwrap_or("").to_string();
|
||||
return;
|
||||
}
|
||||
|
||||
let current = ResourceDef::root_prefix(&self.current_prefix);
|
||||
let joined = current.join(&next);
|
||||
self.current_prefix = joined.pattern().unwrap_or("").to_string();
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) fn prepare_scope_id(&mut self) -> usize {
|
||||
let scope_id = self.introspector.borrow_mut().next_scope_id();
|
||||
self.pending_scope_id = Some(scope_id);
|
||||
scope_id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use super::{Guard, GuardContext};
|
||||
use super::{Guard, GuardContext, GuardDetail};
|
||||
use crate::http::header::Accept;
|
||||
|
||||
/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type.
|
||||
|
|
@ -63,6 +63,37 @@ impl Guard for Acceptable {
|
|||
|
||||
false
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
if self.match_star_star {
|
||||
format!("Acceptable({}, match_star_star=true)", self.mime)
|
||||
} else {
|
||||
format!("Acceptable({})", self.mime)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "experimental-introspection"))]
|
||||
{
|
||||
std::any::type_name::<Self>().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
let mut details = Vec::new();
|
||||
details.push(GuardDetail::Generic(format!("mime={}", self.mime)));
|
||||
if self.match_star_star {
|
||||
details.push(GuardDetail::Generic("match_star_star=true".to_string()));
|
||||
}
|
||||
Some(details)
|
||||
}
|
||||
#[cfg(not(feature = "experimental-introspection"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -96,4 +127,28 @@ mod tests {
|
|||
.match_star_star()
|
||||
.check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
#[test]
|
||||
fn acceptable_guard_details_include_mime() {
|
||||
let guard = Acceptable::new(mime::APPLICATION_JSON).match_star_star();
|
||||
let details = guard.details().expect("missing guard details");
|
||||
|
||||
assert!(details.iter().any(|detail| match detail {
|
||||
GuardDetail::Generic(value) => value == "match_star_star=true",
|
||||
_ => false,
|
||||
}));
|
||||
let expected = format!("mime={}", mime::APPLICATION_JSON);
|
||||
assert!(details.iter().any(|detail| match detail {
|
||||
GuardDetail::Generic(value) => value == &expected,
|
||||
_ => false,
|
||||
}));
|
||||
assert_eq!(
|
||||
guard.name(),
|
||||
format!(
|
||||
"Acceptable({}, match_star_star=true)",
|
||||
mime::APPLICATION_JSON
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use actix_http::{header, uri::Uri, RequestHead, Version};
|
||||
|
||||
use super::{Guard, GuardContext};
|
||||
use super::{Guard, GuardContext, GuardDetail};
|
||||
|
||||
/// Creates a guard that matches requests targeting a specific host.
|
||||
///
|
||||
|
|
@ -117,6 +117,41 @@ impl Guard for HostGuard {
|
|||
// all conditions passed
|
||||
true
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
if let Some(ref scheme) = self.scheme {
|
||||
format!("Host({}, scheme={})", self.host, scheme)
|
||||
} else {
|
||||
format!("Host({})", self.host)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "experimental-introspection"))]
|
||||
{
|
||||
std::any::type_name::<Self>().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
let mut details = vec![GuardDetail::Headers(vec![(
|
||||
"host".to_string(),
|
||||
self.host.clone(),
|
||||
)])];
|
||||
|
||||
if let Some(ref scheme) = self.scheme {
|
||||
details.push(GuardDetail::Generic(format!("scheme={scheme}")));
|
||||
}
|
||||
|
||||
Some(details)
|
||||
}
|
||||
#[cfg(not(feature = "experimental-introspection"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -239,4 +274,23 @@ mod tests {
|
|||
let host = Host("localhost");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
#[test]
|
||||
fn host_guard_details_include_host_and_scheme() {
|
||||
let host = Host("example.com").scheme("https");
|
||||
let details = host.details().expect("missing guard details");
|
||||
|
||||
assert!(details.iter().any(|detail| match detail {
|
||||
GuardDetail::Headers(headers) => headers
|
||||
.iter()
|
||||
.any(|(name, value)| name == "host" && value == "example.com"),
|
||||
_ => false,
|
||||
}));
|
||||
assert!(details.iter().any(|detail| match detail {
|
||||
GuardDetail::Generic(value) => value == "scheme=https",
|
||||
_ => false,
|
||||
}));
|
||||
assert_eq!(host.name(), "Host(example.com, scheme=https)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,14 +66,14 @@ pub use self::{
|
|||
host::{Host, HostGuard},
|
||||
};
|
||||
|
||||
/// Enum to encapsulate various introspection details of a Guard.
|
||||
/// Enum to encapsulate various introspection details of a guard.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GuardDetail {
|
||||
/// Detail associated with HTTP methods.
|
||||
/// Detail associated with explicit HTTP method guards.
|
||||
HttpMethods(Vec<String>),
|
||||
/// Detail associated with headers (header, value).
|
||||
Headers(Vec<(String, String)>),
|
||||
/// Generic detail.
|
||||
/// Generic detail, typically used for compound guard representations.
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +141,9 @@ pub trait Guard {
|
|||
std::any::type_name::<Self>().to_string()
|
||||
}
|
||||
|
||||
/// Returns detailed introspection information.
|
||||
/// Returns detailed introspection information, when available.
|
||||
///
|
||||
/// This is best-effort and may omit complex guard logic.
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
None
|
||||
}
|
||||
|
|
@ -358,7 +360,14 @@ impl<G: Guard> Guard for Not<G> {
|
|||
format!("Not({})", self.0.name())
|
||||
}
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
self.0.details()
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
Some(vec![GuardDetail::Generic(self.name())])
|
||||
}
|
||||
#[cfg(not(feature = "experimental-introspection"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -434,42 +434,58 @@ where
|
|||
{
|
||||
use crate::http::Method;
|
||||
|
||||
let full_paths = crate::introspection::expand_patterns(&config.current_prefix, &rdef);
|
||||
let patterns = rdef
|
||||
.pattern_iter()
|
||||
.map(|pattern| pattern.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let guards_routes = routes.iter().map(|r| r.guards()).collect::<Vec<_>>();
|
||||
|
||||
let pat = rdef.pattern().unwrap_or("").to_string();
|
||||
let full_path = if config.current_prefix.is_empty() {
|
||||
pat.clone()
|
||||
} else {
|
||||
format!(
|
||||
"{}/{}",
|
||||
config.current_prefix.trim_end_matches('/'),
|
||||
pat.trim_start_matches('/')
|
||||
)
|
||||
};
|
||||
let scope_id = config.scope_id_stack.last().copied();
|
||||
let resource_guards: &[Box<dyn Guard>] = guards.as_deref().unwrap_or(&[]);
|
||||
let resource_name = self.name.clone();
|
||||
|
||||
for route_guards in guards_routes {
|
||||
// Log the guards and methods for introspection
|
||||
let guard_names = route_guards.iter().map(|g| g.name()).collect::<Vec<_>>();
|
||||
let methods = route_guards
|
||||
.iter()
|
||||
.flat_map(|g| g.details().unwrap_or_default())
|
||||
.flat_map(|d| {
|
||||
if let crate::guard::GuardDetail::HttpMethods(v) = d {
|
||||
v.into_iter()
|
||||
.filter_map(|s| s.parse::<Method>().ok())
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut guard_names = Vec::new();
|
||||
let mut methods = Vec::new();
|
||||
|
||||
config.introspector.borrow_mut().register_pattern_detail(
|
||||
full_path.clone(),
|
||||
methods,
|
||||
guard_names,
|
||||
true,
|
||||
for guard in resource_guards.iter().chain(route_guards.iter()) {
|
||||
guard_names.push(guard.name());
|
||||
methods.extend(
|
||||
guard
|
||||
.details()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.flat_map(|d| {
|
||||
if let crate::guard::GuardDetail::HttpMethods(v) = d {
|
||||
v.into_iter()
|
||||
.filter_map(|s| s.parse::<Method>().ok())
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let guard_details = crate::introspection::guard_reports_from_iter(
|
||||
resource_guards.iter().chain(route_guards.iter()),
|
||||
);
|
||||
|
||||
for full_path in &full_paths {
|
||||
let info = crate::introspection::RouteInfo::new(
|
||||
full_path.clone(),
|
||||
methods.clone(),
|
||||
guard_names.clone(),
|
||||
guard_details.clone(),
|
||||
patterns.clone(),
|
||||
resource_name.clone(),
|
||||
);
|
||||
config
|
||||
.introspector
|
||||
.borrow_mut()
|
||||
.register_route(info, scope_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -387,7 +387,11 @@ where
|
|||
|
||||
// Update the prefix for the nested scope
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
cfg.update_prefix(&self.rdef);
|
||||
{
|
||||
let scope_id = config.prepare_scope_id();
|
||||
cfg.scope_id_stack.push(scope_id);
|
||||
cfg.update_prefix(&self.rdef);
|
||||
}
|
||||
|
||||
self.services
|
||||
.into_iter()
|
||||
|
|
@ -395,8 +399,17 @@ where
|
|||
|
||||
let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef));
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
let origin_scope = cfg.current_prefix.clone();
|
||||
|
||||
// external resources
|
||||
for mut rdef in mem::take(&mut self.external) {
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
cfg.introspector
|
||||
.borrow_mut()
|
||||
.register_external(&rdef, &origin_scope);
|
||||
}
|
||||
rmap.add(&mut rdef, None);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
#![cfg(feature = "experimental-introspection")]
|
||||
|
||||
use actix_web::{guard, test, web, App, HttpResponse};
|
||||
|
||||
async fn introspection_handler(
|
||||
tree: web::Data<actix_web::introspection::IntrospectionTree>,
|
||||
) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(tree.report_as_json())
|
||||
}
|
||||
|
||||
async fn externals_handler(
|
||||
tree: web::Data<actix_web::introspection::IntrospectionTree>,
|
||||
) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(tree.report_externals_as_json())
|
||||
}
|
||||
|
||||
fn find_item<'a>(items: &'a [serde_json::Value], path: &str) -> &'a serde_json::Value {
|
||||
items
|
||||
.iter()
|
||||
.find(|item| item.get("full_path").and_then(|v| v.as_str()) == Some(path))
|
||||
.unwrap_or_else(|| panic!("missing route for {path}"))
|
||||
}
|
||||
|
||||
fn find_external<'a>(items: &'a [serde_json::Value], name: &str) -> &'a serde_json::Value {
|
||||
items
|
||||
.iter()
|
||||
.find(|item| item.get("name").and_then(|v| v.as_str()) == Some(name))
|
||||
.unwrap_or_else(|| panic!("missing external resource for {name}"))
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn introspection_report_includes_details_and_metadata() {
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.external_resource("app-external", "https://example.com/{id}")
|
||||
.service(
|
||||
web::resource(["/alpha", "/beta"])
|
||||
.name("multi")
|
||||
.route(web::get().to(HttpResponse::Ok)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/guarded")
|
||||
.guard(guard::Header("accept", "text/plain"))
|
||||
.route(web::get().to(HttpResponse::Ok)),
|
||||
)
|
||||
.service(
|
||||
web::scope("/scoped")
|
||||
.guard(guard::Header("x-scope", "1"))
|
||||
.configure(|cfg| {
|
||||
cfg.external_resource("scope-external", "https://scope.example/{id}");
|
||||
})
|
||||
.service(web::resource("/item").route(web::get().to(HttpResponse::Ok))),
|
||||
)
|
||||
.service(web::resource("/introspection").route(web::get().to(introspection_handler)))
|
||||
.service(
|
||||
web::resource("/introspection/externals").route(web::get().to(externals_handler)),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = test::TestRequest::get().uri("/introspection").to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
assert!(resp.status().is_success());
|
||||
|
||||
let body = test::read_body(resp).await;
|
||||
let items: Vec<serde_json::Value> =
|
||||
serde_json::from_slice(&body).expect("invalid introspection json");
|
||||
|
||||
let alpha = find_item(&items, "/alpha");
|
||||
let patterns = alpha
|
||||
.get("patterns")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("patterns missing");
|
||||
let patterns = patterns
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(patterns.contains(&"/alpha"));
|
||||
assert!(patterns.contains(&"/beta"));
|
||||
assert_eq!(
|
||||
alpha.get("resource_name").and_then(|v| v.as_str()),
|
||||
Some("multi")
|
||||
);
|
||||
assert_eq!(
|
||||
alpha.get("resource_type").and_then(|v| v.as_str()),
|
||||
Some("resource")
|
||||
);
|
||||
|
||||
let guarded = find_item(&items, "/guarded");
|
||||
let guards = guarded
|
||||
.get("guards")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("guards missing");
|
||||
assert!(guards
|
||||
.iter()
|
||||
.any(|v| v.as_str() == Some("Header(accept, text/plain)")));
|
||||
|
||||
let guard_details = guarded
|
||||
.get("guards_detail")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("guards_detail missing");
|
||||
assert!(!guard_details.is_empty());
|
||||
|
||||
let alpha_guards = alpha
|
||||
.get("guards")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("alpha guards missing");
|
||||
let alpha_guard_details = alpha
|
||||
.get("guards_detail")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("alpha guards_detail missing");
|
||||
assert!(alpha_guards.is_empty());
|
||||
assert!(!alpha_guard_details.is_empty());
|
||||
|
||||
let scoped = find_item(&items, "/scoped");
|
||||
assert_eq!(
|
||||
scoped.get("resource_type").and_then(|v| v.as_str()),
|
||||
Some("scope")
|
||||
);
|
||||
let scoped_guards = scoped
|
||||
.get("guards")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("scoped guards missing");
|
||||
assert!(scoped_guards
|
||||
.iter()
|
||||
.any(|v| v.as_str() == Some("Header(x-scope, 1)")));
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/introspection/externals")
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
assert!(resp.status().is_success());
|
||||
|
||||
let body = test::read_body(resp).await;
|
||||
let externals: Vec<serde_json::Value> =
|
||||
serde_json::from_slice(&body).expect("invalid externals json");
|
||||
|
||||
let app_external = find_external(&externals, "app-external");
|
||||
let app_patterns = app_external
|
||||
.get("patterns")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("app external patterns missing");
|
||||
assert!(app_patterns
|
||||
.iter()
|
||||
.any(|v| v.as_str() == Some("https://example.com/{id}")));
|
||||
assert_eq!(
|
||||
app_external.get("origin_scope").and_then(|v| v.as_str()),
|
||||
Some("/")
|
||||
);
|
||||
|
||||
let scope_external = find_external(&externals, "scope-external");
|
||||
let scope_patterns = scope_external
|
||||
.get("patterns")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("scope external patterns missing");
|
||||
assert!(scope_patterns
|
||||
.iter()
|
||||
.any(|v| v.as_str() == Some("https://scope.example/{id}")));
|
||||
assert_eq!(
|
||||
scope_external.get("origin_scope").and_then(|v| v.as_str()),
|
||||
Some("/scoped")
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue