• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divviup / divviup-api / 25528952740

08 May 2026 12:06AM UTC coverage: 57.286% (-1.3%) from 58.564%
25528952740

Pull #2247

github

web-flow
Merge 6f819cf00 into 91680da33
Pull Request #2247: Migrate from Trillium [part 7C]: admin routes and Trillium router removal

42 of 63 new or added lines in 5 files covered. (66.67%)

111 existing lines in 12 files now uncovered.

4352 of 7597 relevant lines covered (57.29%)

64.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.14
/src/handler.rs
1
pub(crate) mod account_bearer_token;
2
#[cfg(assets)]
3
pub(crate) mod assets;
4
pub(crate) mod cors;
5
pub(crate) mod custom_mime_types;
6
pub(crate) mod error;
7
pub(crate) mod extract;
8
pub(crate) mod logger;
9
pub(crate) mod oauth2;
10
pub(crate) mod opentelemetry;
11
pub(crate) mod origin_router;
12
pub(crate) mod session_store;
13

14
pub(crate) mod proxy;
15

16
use crate::{clients::Auth0Client, routes, routes::axum_routes, Config, Crypter, Db, FeatureFlags};
17
use trillium_client::Client;
18

19
use axum::extract::{DefaultBodyLimit, FromRef};
20
use axum::http::{header, HeaderValue};
21
use cors::axum_cors_layer;
22
use error::ErrorHandler;
23
use logger::logger;
24
use oauth2::OauthClient;
25
use proxy::AxumProxy;
26
use session_store::axum_session_layer;
27
use std::{borrow::Cow, net::Ipv6Addr, net::SocketAddr, sync::Arc};
28
use tokio::net::TcpListener;
29
use tower::ServiceBuilder;
30
use tower_http::compression::CompressionLayer;
31
use tower_http::set_header::SetResponseHeaderLayer;
32
use tower_http::trace::TraceLayer;
33
use trillium::{Handler, Info};
34
use trillium_caching_headers::caching_headers;
35
use trillium_conn_id::conn_id;
36
use trillium_forwarding::Forwarding;
37
use trillium_macros::Handler;
38

39
pub use error::Error;
40
pub use origin_router::origin_router;
41

42
use self::opentelemetry::opentelemetry;
43

44
#[cfg(all(assets, feature = "otlp-trace"))]
45
use trillium_opentelemetry::global::instrument_handler;
46
#[cfg(all(assets, not(feature = "otlp-trace")))]
47
fn instrument_handler(handler: impl Handler) -> impl Handler {
278✔
48
    handler
278✔
49
}
278✔
50

51
/// Shared state for the Axum side of the application during migration.
52
#[derive(Clone, Debug)]
53
pub struct AxumAppState {
54
    pub(crate) db: Db,
55
    pub(crate) config: Arc<Config>,
56
    pub(crate) auth0_client: Auth0Client,
57
    pub(crate) oauth_client: OauthClient,
58
    pub(crate) crypter: Crypter,
59
    pub(crate) feature_flags: FeatureFlags,
60
    pub(crate) client: Client,
61
}
62

63
impl FromRef<AxumAppState> for Db {
64
    fn from_ref(state: &AxumAppState) -> Self {
777✔
65
        state.db.clone()
777✔
66
    }
777✔
67
}
68

69
impl FromRef<AxumAppState> for Arc<Config> {
70
    fn from_ref(state: &AxumAppState) -> Self {
3✔
71
        state.config.clone()
3✔
72
    }
3✔
73
}
74

75
impl FromRef<AxumAppState> for Auth0Client {
76
    fn from_ref(state: &AxumAppState) -> Self {
×
77
        state.auth0_client.clone()
×
78
    }
×
79
}
80

81
impl FromRef<AxumAppState> for OauthClient {
82
    fn from_ref(state: &AxumAppState) -> Self {
2✔
83
        state.oauth_client.clone()
2✔
84
    }
2✔
85
}
86

87
impl FromRef<AxumAppState> for Crypter {
88
    fn from_ref(state: &AxumAppState) -> Self {
65✔
89
        state.crypter.clone()
65✔
90
    }
65✔
91
}
92

93
impl FromRef<AxumAppState> for FeatureFlags {
94
    fn from_ref(state: &AxumAppState) -> Self {
20✔
95
        state.feature_flags
20✔
96
    }
20✔
97
}
98

99
impl FromRef<AxumAppState> for Client {
100
    fn from_ref(state: &AxumAppState) -> Self {
65✔
101
        state.client.clone()
65✔
102
    }
65✔
103
}
104

105
#[derive(Handler, Debug)]
106
pub struct DivviupApi {
107
    #[handler(except = init)]
108
    handler: Box<dyn Handler>,
109
    db: Db,
110
    config: Arc<Config>,
111
    axum_addr: SocketAddr,
112
}
113

114
impl DivviupApi {
115
    pub async fn init(&mut self, info: &mut Info) {
278✔
116
        *info.server_description_mut() = format!("divviup-api {}", env!("CARGO_PKG_VERSION"));
278✔
117
        *info.listener_description_mut() = format!(
278✔
118
            "api url: {}\n             app url: {}\n             axum: {}\n",
119
            self.config.api_url, self.config.app_url, self.axum_addr,
278✔
120
        );
121
        self.handler.init(info).await
278✔
122
    }
278✔
123

124
    pub async fn new(config: Config) -> Self {
278✔
125
        let config = Arc::new(config);
278✔
126
        let db = Db::connect(config.database_url.as_ref()).await;
278✔
127

128
        // Spawn the Axum server on an ephemeral port. Routes will be migrated
129
        // here incrementally.
130
        let auth0_client = Auth0Client::new(&config);
278✔
131
        let axum_state = AxumAppState {
278✔
132
            db: db.clone(),
278✔
133
            config: config.clone(),
278✔
134
            auth0_client: auth0_client.clone(),
278✔
135
            oauth_client: OauthClient::new(&config.oauth_config()),
278✔
136
            crypter: config.crypter.clone(),
278✔
137
            feature_flags: config.feature_flags(),
278✔
138
            client: config.client.clone(),
278✔
139
        };
278✔
140
        // Middleware stack in logical order (outermost first), matching the
141
        // Trillium api() handler chain.
142
        let middleware = ServiceBuilder::new()
278✔
143
            .layer(TraceLayer::new_for_http())
278✔
144
            .layer(DefaultBodyLimit::max(1024 * 1024))
278✔
145
            .layer(CompressionLayer::new())
278✔
146
            .layer(SetResponseHeaderLayer::if_not_present(
278✔
147
                header::CACHE_CONTROL,
278✔
148
                HeaderValue::from_static("private, must-revalidate"),
278✔
149
            ))
150
            .layer(axum_cors_layer(&config))
278✔
151
            .layer(axum_session_layer(db.clone(), &config.session_secrets));
278✔
152

153
        #[cfg(feature = "test-header-injection")]
154
        let middleware =
278✔
155
            middleware.layer(axum::middleware::from_fn(inject_integration_testing_user));
278✔
156

157
        let axum_router = axum::Router::new()
278✔
158
            // Temporary test endpoint to verify the proxy bridge works.
159
            // TODO: Remove once enough routes have migrated to make it redundant.
160
            .route(
278✔
161
                "/internal/test/axum_ready",
278✔
162
                axum::routing::get(|| async { "axum OK" }),
278✔
163
            )
164
            .route("/health", axum::routing::get(routes::health_check))
278✔
165
            .route("/login", axum::routing::get(oauth2::redirect))
278✔
166
            .route("/logout", axum::routing::get(oauth2::logout))
278✔
167
            .route("/callback", axum::routing::get(oauth2::callback))
278✔
168
            .nest("/api", axum_routes::api_router(&axum_state))
278✔
169
            .layer(middleware)
278✔
170
            .with_state(axum_state);
278✔
171
        let axum_listener = TcpListener::bind((Ipv6Addr::LOCALHOST, 0))
278✔
172
            .await
278✔
173
            .expect("failed to bind Axum listener on IPv6 loopback");
278✔
174
        let axum_addr = axum_listener
278✔
175
            .local_addr()
278✔
176
            .expect("failed to get Axum listener address");
278✔
177
        // TODO: Wire graceful shutdown into axum::serve(...).with_graceful_shutdown()
178
        // so that in-flight requests are drained when the Trillium server stops.
179
        tokio::spawn(async move {
278✔
180
            if let Err(e) = axum::serve(axum_listener, axum_router).await {
278✔
181
                log::error!("axum server error: {e}");
×
182
            }
×
183
        });
×
184

185
        let proxy = AxumProxy::new(axum_addr);
278✔
186

187
        Self {
278✔
188
            handler: Box::new((
278✔
189
                conn_id(),
278✔
190
                Forwarding::trust_always(),
278✔
191
                opentelemetry(),
278✔
192
                caching_headers(),
278✔
193
                logger(),
278✔
194
                #[cfg(assets)]
278✔
195
                instrument_handler(assets::static_assets(&config)),
278✔
196
                #[cfg(feature = "test-header-injection")]
278✔
197
                inject_test_user_trillium,
278✔
198
                proxy,
278✔
199
                ErrorHandler,
278✔
200
            )),
278✔
201
            db,
278✔
202
            config,
278✔
203
            axum_addr,
278✔
204
        }
278✔
205
    }
278✔
206

207
    pub fn db(&self) -> &Db {
1,515✔
208
        &self.db
1,515✔
209
    }
1,515✔
210

211
    pub fn config(&self) -> &Config {
21✔
212
        &self.config
21✔
213
    }
21✔
214

215
    pub fn crypter(&self) -> &crate::Crypter {
231✔
216
        &self.config.crypter
231✔
217
    }
231✔
218

219
    #[expect(dead_code)] // Scaffolded for later migration parts.
220
    pub(crate) fn axum_addr(&self) -> SocketAddr {
×
221
        self.axum_addr
×
222
    }
×
223
}
224

225
impl AsRef<Db> for DivviupApi {
226
    fn as_ref(&self) -> &Db {
×
227
        &self.db
×
228
    }
×
229
}
230

231
/// Trillium-side test shim: if a `User` was injected via `.with_state()`
232
/// (legacy test pattern), serialize it into the `X-Integration-Testing-User`
233
/// header so the proxy forwards it to Axum.
234
// TODO: remove in Part 8 (Trillium removal)
235
#[cfg(feature = "test-header-injection")]
236
async fn inject_test_user_trillium(mut conn: trillium::Conn) -> trillium::Conn {
273✔
237
    if let Some(json) = conn
273✔
238
        .state::<crate::User>()
273✔
239
        .and_then(|u| serde_json::to_string(u).ok())
273✔
240
    {
149✔
241
        conn.request_headers_mut()
149✔
242
            .insert("x-integration-testing-user", json);
149✔
243
    }
149✔
244
    conn
273✔
245
}
273✔
246

247
/// Axum-side test shim: if the request carries an `X-Integration-Testing-User`
248
/// header with a JSON-encoded [`crate::User`], place the user in request
249
/// extensions so [`crate::User`]'s extractor picks it up without a real
250
/// session.
251
///
252
/// Only compiled under `--features test-header-injection` (enabled by
253
/// `test-support`). Never compiled into deployed builds.
254
#[cfg(feature = "test-header-injection")]
255
async fn inject_integration_testing_user(
273✔
256
    mut request: axum::extract::Request,
273✔
257
    next: axum::middleware::Next,
273✔
258
) -> axum::response::Response {
273✔
259
    if let Some(user) = request
273✔
260
        .headers()
273✔
261
        .get("x-integration-testing-user")
273✔
262
        .and_then(|v| serde_json::from_slice::<crate::User>(v.as_bytes()).ok())
273✔
263
    {
151✔
264
        request.extensions_mut().insert(user);
151✔
265
    }
151✔
266
    next.run(request).await
273✔
267
}
273✔
268

269
#[derive(Handler, Debug, Clone)]
270
pub struct NamedHandler<H>(#[handler(except = name)] H, Cow<'static, str>);
271
impl<H: Handler> NamedHandler<H> {
UNCOV
272
    fn name(&self) -> Cow<'static, str> {
×
UNCOV
273
        self.1.clone()
×
UNCOV
274
    }
×
275

UNCOV
276
    pub fn new(name: impl Into<Cow<'static, str>>, handler: H) -> Self {
×
UNCOV
277
        Self(handler, name.into())
×
UNCOV
278
    }
×
279
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc