use std::sync::Arc;
use axum::{Router, http::Method, routing::MethodRouter};
use http::header::{
ACCEPT_ENCODING, AUTHORIZATION, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, DNT, HOST, ORIGIN,
REFERER,
};
use pib_service_facade::Service;
use tower_http::{
auth::AsyncRequireAuthorizationLayer,
cors::{AllowOrigin, CorsLayer},
};
use crate::Auth;
mod body;
mod config;
mod get;
mod me;
mod user;
pub(super) async fn router<E: Into<pib_service_facade::Error> + Send + Sync + 'static>(
auth: Auth,
service: Arc<dyn Service<E>>,
) -> Router
where
crate::Error: From<E>,
{
let cors = CorsLayer::new()
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::PATCH,
])
.allow_headers([
AUTHORIZATION,
CONTENT_TYPE,
ACCEPT_ENCODING,
CONTENT_LENGTH,
ORIGIN,
REFERER,
CONNECTION,
DNT,
HOST,
])
.allow_origin(AllowOrigin::mirror_request());
let public = Router::new()
.route("/", MethodRouter::new().get(get::handle))
.route("/config", MethodRouter::new().get(config::get::handle));
let private = Router::new()
.route(
"/me/profile",
MethodRouter::new()
.get(me::profile::get::handle)
.patch(me::profile::patch::handle),
)
.route("/user", MethodRouter::new().get(user::get::handle))
.route(
"/body",
MethodRouter::new()
.get(body::get::handle)
.post(body::post::handle),
)
.route(
"/body/{body_id}",
MethodRouter::new()
.get(body::by_id::get::handle)
.patch(body::by_id::patch::handle),
)
.route(
"/body/{body_id}/permission",
MethodRouter::new()
.get(body::by_id::permission::get::handle)
.post(body::by_id::permission::post::handle),
)
.route(
"/body/{body_id}/organization",
MethodRouter::new()
.get(body::by_id::organization::get::handle)
.post(body::by_id::organization::post::handle),
)
.route(
"/body/{body_id}/organization/{organization_id}",
MethodRouter::new()
.get(body::by_id::organization::by_id::get::handle)
.patch(body::by_id::organization::by_id::patch::handle),
)
.route(
"/body/{body_id}/person",
MethodRouter::new()
.get(body::by_id::person::get::handle)
.post(body::by_id::person::post::handle),
)
.route(
"/body/{body_id}/person/{person_id}",
MethodRouter::new()
.get(body::by_id::person::by_id::get::handle)
.patch(body::by_id::person::by_id::patch::handle),
)
.route(
"/body/{body_id}/meeting",
MethodRouter::new()
.get(body::by_id::meeting::get::handle)
.post(body::by_id::meeting::post::handle),
)
.route(
"/body/{body_id}/meeting/{meeting_id}",
MethodRouter::new()
.get(body::by_id::meeting::by_id::get::handle)
.patch(body::by_id::meeting::by_id::patch::handle),
)
.route(
"/body/{body_id}/meeting/{meeting_id}/agenda_item",
MethodRouter::new()
.get(body::by_id::meeting::by_id::agenda_item::get::handle)
.post(body::by_id::meeting::by_id::agenda_item::post::handle),
)
.route(
"/body/{body_id}/meeting/{meeting_id}/agenda_item/{agenda_item_id}",
MethodRouter::new()
.get(body::by_id::meeting::by_id::agenda_item::by_id::get::handle)
.patch(body::by_id::meeting::by_id::agenda_item::by_id::patch::handle),
)
.layer(AsyncRequireAuthorizationLayer::new(auth));
Router::new()
.merge(public)
.merge(private)
.layer(cors)
.with_state(service)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use axum_test::TestServer;
use mockall::predicate::eq;
use pib_service_api_auth::{
MockApiAuth,
user::{Issuer, OidcSub, UserInfo},
};
use pib_service_api_types::{
self as types, body::by_id::meeting::MeetingState, config::oidc::OidcConfig, user::User,
};
use pib_service_core_types::{BodyId, MeetingId, OrganizationId, UserId};
use pib_service_facade::MockService;
use crate::auth::Auth;
use super::router;
fn example_user_info() -> UserInfo {
UserInfo {
id: UserId::from_u128(0x1111),
issuer: Issuer("https://auth.example.com/".to_string()),
sub: OidcSub("max".to_string()),
display_name: None,
is_superuser: false,
}
}
#[tokio::test]
async fn success_get() {
let api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
service
.expect_handle_get()
.times(1)
.return_once(|| Ok(types::get::ResponseBody {}));
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
let response = server
.get("/")
.await
.assert_status_success()
.json::<types::get::ResponseBody>();
assert_eq!(response, types::get::ResponseBody {})
}
#[tokio::test]
async fn success_config_get() {
let api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
let oidc = OidcConfig {
issuer: "https://auth.example.com/".to_string(),
client_id: "the-client-id".to_string(),
client_secret: "the-client-secret".to_string(),
};
{
let oidc = oidc.clone();
service
.expect_handle_config_get()
.times(1)
.return_once(|| Ok(types::config::get::ResponseBody { oidc }));
}
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
let response = server
.get("/config")
.await
.assert_status_success()
.json::<types::config::get::ResponseBody>();
assert_eq!(response, types::config::get::ResponseBody { oidc })
}
#[tokio::test]
async fn success_me_profile_get() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
service
.expect_handle_profile_me_get()
.times(1)
.return_once(|_| {
Ok(types::me::profile::get::ResponseBody {
display_name: Some("Max".to_string()),
})
});
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
let response = server
.get("/me/profile")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_success()
.json::<types::me::profile::get::ResponseBody>();
assert_eq!(
response,
types::me::profile::get::ResponseBody {
display_name: Some("Max".to_string())
}
)
}
#[tokio::test]
async fn success_me_profile_patch() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
service
.expect_handle_profile_me_patch()
.times(1)
.return_once(|_, _| Ok(()));
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
server
.patch("/me/profile")
.json(&types::me::profile::patch::RequestBody {
display_name: Some(Some("hello".to_string())),
})
.add_header("Authorization", "xxyyzz")
.await
.assert_status_see_other()
.assert_header("Location", "/me/profile");
}
#[tokio::test]
async fn success_user_get() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
let user = User {
id: UserId::from_u128(0x1111),
sub: "theuser".to_string(),
display_name: None,
is_superuser: true,
};
{
let user = user.clone();
service
.expect_handle_user_get()
.times(1)
.return_once(|_, _| {
Ok(types::user::get::ResponseBody(
pib_service_core_types::DataPage {
data: vec![user],
pagination: pib_service_core_types::DataPagePagination {
total_elements: 11,
elements_per_page: 12,
current_page: 13,
total_pages: 14,
},
},
))
});
}
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
let response = server
.get("/user")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_success()
.json::<types::user::get::ResponseBody>();
assert_eq!(
response,
types::user::get::ResponseBody(pib_service_core_types::DataPage {
data: vec![user],
pagination: pib_service_core_types::DataPagePagination {
total_elements: 11,
elements_per_page: 12,
current_page: 13,
total_pages: 14,
}
})
)
}
#[tokio::test]
async fn success_body_get() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
service
.expect_handle_body_get()
.times(1)
.return_once(|_, _| {
Ok(types::body::get::ResponseBody(
pib_service_core_types::DataPage::empty(15.try_into().unwrap()),
))
});
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
let response = server
.get("/body")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_success()
.json::<types::body::get::ResponseBody>();
assert_eq!(
response,
types::body::get::ResponseBody(pib_service_core_types::DataPage::empty(
15.try_into().unwrap()
))
)
}
#[tokio::test]
async fn success_body_post() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
service
.expect_handle_body_post()
.times(1)
.return_once(|_, _| Ok(BodyId::from_u128(0x94949494)));
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
server
.post("/body")
.json(&types::body::post::RequestBody {
short_name: None,
name: "Hintertupfing".parse().unwrap(),
website: None,
license: None,
license_valid_since: None,
oparl_since: None,
ags: "12345".to_string(),
rgs: None,
contact_email: None,
contact_name: None,
classification: None,
is_public: true,
})
.add_header("Authorization", "xxyyzz")
.await
.assert_status_see_other()
.assert_header("Location", "/body/00000000-0000-0000-0000-000094949494");
}
#[tokio::test]
async fn success_body_by_id_get() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
let response_body = types::body::by_id::get::ResponseBody(types::body::Body {
id: BodyId::from_u128(0x17593122_2260_450e_a869_fe160e69ceb2),
created: "2026-04-07T05:06:07+01:00".parse().unwrap(),
modified: "2026-04-08T05:06:07+01:00".parse().unwrap(),
short_name: None,
name: "Hintertupfing".parse().unwrap(),
website: None,
license: None,
license_valid_since: None,
oparl_since: None,
ags: "12345".to_string(),
rgs: None,
contact_email: None,
contact_name: None,
classification: None,
is_public: true,
});
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
{
let response_body = response_body.clone();
service
.expect_handle_body_by_id_get()
.times(1)
.return_once(|_, _| Ok(response_body));
}
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
let response = server
.get("/body/17593122-2260-450e-a869-fe160e69ceb2")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_success()
.json::<types::body::by_id::get::ResponseBody>();
assert_eq!(response, response_body);
}
#[tokio::test]
async fn success_body_by_id_meeting_get() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
service
.expect_handle_body_by_id_meeting_get()
.times(1)
.return_once(|_, _, _| {
Ok(types::body::by_id::meeting::get::ResponseBody(
pib_service_core_types::DataPage {
data: vec![],
pagination: pib_service_core_types::DataPagePagination {
total_elements: 11,
current_page: 12,
elements_per_page: 13,
total_pages: 14,
},
},
))
});
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
let response = server
.get("/body/152cf529-df96-45ba-833a-54509a26d064/meeting")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_success()
.json::<types::body::by_id::meeting::get::ResponseBody>();
assert_eq!(
response,
types::body::by_id::meeting::get::ResponseBody(pib_service_core_types::DataPage {
data: vec![],
pagination: pib_service_core_types::DataPagePagination {
total_elements: 11,
current_page: 12,
elements_per_page: 13,
total_pages: 14,
}
})
)
}
#[tokio::test]
async fn success_body_by_id_meeting_post() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
service
.expect_handle_body_by_id_meeting_post()
.times(1)
.return_once(|_, _, _| Ok(MeetingId::from_u128(0x131313)));
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
server
.post("/body/152cf529-df96-45ba-833a-54509a26d064/meeting")
.json(&types::body::by_id::meeting::post::RequestBody {
organization_id: OrganizationId::from_u128(0x999999999),
name: Some("Hintertupfing".parse().unwrap()),
license: None,
meeting_state: "planned".to_string(),
keyword: None,
start: None,
end: None,
web: None,
})
.add_header("Authorization", "xxyyzz")
.await
.assert_status_see_other()
.assert_header(
"Location",
"/body/152cf529-df96-45ba-833a-54509a26d064/meeting/00000000-0000-0000-0000-000000131313",
);
}
#[tokio::test]
async fn success_body_by_id_meeting_by_id_get() {
let mut api_auth = MockApiAuth::new();
let mut service = MockService::<pib_service_facade::Error>::new();
let response_body = types::body::by_id::meeting::by_id::get::ResponseBody(
types::body::by_id::meeting::Meeting {
id: MeetingId::from_u128(0xcf4cb545_4e10_4205_8cb4_7ddcbba0e0dd),
body_id: BodyId::from_u128(0x17593122_2260_450e_a869_fe160e69ceb2),
organization_id: OrganizationId::from_u128(0xb991c4e7_1283_4997_b042_f962d19895cf),
created: "2026-04-07T05:06:07+01:00".parse().unwrap(),
modified: "2026-04-08T05:06:07+01:00".parse().unwrap(),
name: None,
meeting_state: MeetingState::Appointed,
start: None,
end: None,
license: None,
web: None,
},
);
api_auth
.expect_authorize()
.times(1)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.return_once(|_| Ok(Some(example_user_info())));
{
let response_body = response_body.clone();
service
.expect_handle_body_by_id_meeting_by_id_get()
.times(1)
.return_once(|_, _, _| Ok(response_body));
}
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
let response = server
.get("/body/17593122-2260-450e-a869-fe160e69ceb2/meeting/cf4cb545-4e10-4205-8cb4-7ddcbba0e0dd")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_success()
.json::<types::body::by_id::meeting::by_id::get::ResponseBody>();
assert_eq!(response, response_body);
}
#[tokio::test]
async fn forbid() {
let mut api_auth = MockApiAuth::new();
let service = MockService::<pib_service_facade::Error>::new();
api_auth
.expect_authorize()
.times(1..)
.with(eq(http::HeaderValue::from_static("xxyyzz")))
.returning(|_| Ok(None));
let app = router(Auth::new(Arc::new(api_auth)), Arc::new(service)).await;
let server = TestServer::new(app);
server
.get("/me/profile")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_unauthorized();
server
.patch("/me/profile")
.json(&types::me::profile::patch::RequestBody {
display_name: Some(Some("hello".to_string())),
})
.add_header("Authorization", "xxyyzz")
.await
.assert_status_unauthorized();
server
.get("/body")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_unauthorized();
server
.post("/body")
.json(&types::body::post::RequestBody {
short_name: None,
name: "hello".parse().unwrap(),
website: None,
license: None,
license_valid_since: None,
oparl_since: None,
ags: "12345".to_string(),
rgs: None,
contact_email: None,
contact_name: None,
classification: None,
is_public: true,
})
.add_header("Authorization", "xxyyzz")
.await
.assert_status_unauthorized();
server
.get("/body/190c9a32-6993-462c-afcb-9f11ad7e1f33")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_unauthorized();
server
.get("/body/190c9a32-6993-462c-afcb-9f11ad7e1f33/meeting")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_unauthorized();
server
.post("/body/190c9a32-6993-462c-afcb-9f11ad7e1f33/meeting")
.json(&types::body::by_id::meeting::post::RequestBody {
organization_id: OrganizationId::from_u128(0x114488),
name: None,
license: None,
meeting_state: "planned".to_string(),
keyword: None,
start: None,
end: None,
web: None,
})
.add_header("Authorization", "xxyyzz")
.await
.assert_status_unauthorized();
server
.get("/body/190c9a32-6993-462c-afcb-9f11ad7e1f33/meeting/0cec81a2-d5d9-4c61-a970-e752bbca7783")
.add_header("Authorization", "xxyyzz")
.await
.assert_status_unauthorized();
}
}