Skip to main content

kellnr_web_ui/
ui.rs

1use axum::Json;
2use axum::extract::{Path, Query, State};
3use axum::http::StatusCode;
4use kellnr_appstate::{AppState, DbState, SettingsState};
5use kellnr_common::crate_data::CrateData;
6use kellnr_common::crate_overview::CrateOverview;
7use kellnr_common::normalized_name::NormalizedName;
8use kellnr_common::original_name::OriginalName;
9use kellnr_common::version::Version;
10use kellnr_db::error::DbError;
11use kellnr_settings::{Settings, SourceMap, compile_time_config};
12use serde::{Deserialize, Serialize};
13use tracing::error;
14use utoipa::ToSchema;
15
16use crate::error::RouteError;
17use crate::session::{AdminUser, MaybeUser};
18
19/// Settings response that includes source tracking.
20/// This wrapper is needed because Settings uses `#[serde(skip)]` on sources
21/// to prevent it from being serialized to TOML config files.
22#[derive(Serialize, Deserialize)]
23pub struct SettingsResponse {
24    #[serde(flatten)]
25    pub settings: Settings,
26    pub sources: SourceMap,
27    pub defaults: Settings,
28}
29
30/// Get Kellnr settings (admin only)
31#[utoipa::path(
32    get,
33    path = "/settings",
34    tag = "ui",
35    responses(
36        (status = 200, description = "Kellnr settings with source tracking"),
37        (status = 403, description = "Admin access required")
38    ),
39    security(("session_cookie" = []))
40)]
41#[allow(clippy::unused_async)] // part of the router
42pub async fn settings(
43    _user: AdminUser,
44    State(settings): SettingsState,
45) -> Result<Json<SettingsResponse>, RouteError> {
46    Ok(Json(SettingsResponse {
47        sources: settings.sources.clone(),
48        settings: (*settings).clone(),
49        defaults: Settings::default(),
50    }))
51}
52
53#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, ToSchema)]
54pub struct DocsEnabledResponse {
55    pub enabled: bool,
56}
57
58/// Check if documentation generation is enabled
59#[utoipa::path(
60    get,
61    path = "/docs_enabled",
62    tag = "ui",
63    responses(
64        (status = 200, description = "Documentation generation status", body = DocsEnabledResponse)
65    )
66)]
67#[allow(clippy::unused_async)] // part of the router
68pub async fn docs_enabled(State(settings): SettingsState) -> Json<DocsEnabledResponse> {
69    Json(DocsEnabledResponse {
70        enabled: settings.docs.enabled,
71    })
72}
73
74#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)]
75pub struct KellnrVersion {
76    pub version: String,
77}
78
79/// Get Kellnr version
80#[utoipa::path(
81    get,
82    path = "/version",
83    tag = "ui",
84    responses(
85        (status = 200, description = "Kellnr version", body = KellnrVersion)
86    )
87)]
88#[allow(clippy::unused_async)] // part of the router
89pub async fn kellnr_version() -> Json<KellnrVersion> {
90    Json(KellnrVersion {
91        version: compile_time_config::KELLNR_COMPTIME__VERSION.to_string(),
92    })
93}
94
95#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema, utoipa::IntoParams)]
96pub struct CratesParams {
97    page: Option<u64>,
98    page_size: Option<u64>,
99    cache: Option<bool>,
100}
101
102#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)]
103pub struct Pagination {
104    crates: Vec<CrateOverview>,
105    page_size: u64,
106    page: u64,
107}
108
109/// Get paginated list of crates
110#[utoipa::path(
111    get,
112    path = "/crates",
113    tag = "ui",
114    params(CratesParams),
115    responses(
116        (status = 200, description = "Paginated crate list", body = Pagination)
117    )
118)]
119pub async fn crates(Query(params): Query<CratesParams>, State(db): DbState) -> Json<Pagination> {
120    let page_size = params.page_size.unwrap_or(10);
121    let page = params.page.unwrap_or(0);
122    let cache = params.cache.unwrap_or(false);
123    let crates = db
124        .get_crate_overview_list(page_size, page_size * page, cache)
125        .await
126        .unwrap_or_default();
127
128    Json(Pagination {
129        crates,
130        page_size,
131        page,
132    })
133}
134
135#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema, utoipa::IntoParams)]
136pub struct SearchParams {
137    name: OriginalName,
138    cache: Option<bool>,
139}
140
141/// Search for crates by name
142#[utoipa::path(
143    get,
144    path = "/search",
145    tag = "ui",
146    params(SearchParams),
147    responses(
148        (status = 200, description = "Search results", body = Pagination)
149    )
150)]
151pub async fn search(Query(params): Query<SearchParams>, State(db): DbState) -> Json<Pagination> {
152    let crates = db
153        .search_in_crate_name(&params.name, params.cache.unwrap_or(false))
154        .await
155        .unwrap_or_default();
156    Json(Pagination {
157        page_size: crates.len() as u64,
158        page: 0, // Return everything as one page
159        crates,
160    })
161}
162
163#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema, utoipa::IntoParams)]
164pub struct CrateDataParams {
165    name: OriginalName,
166}
167
168/// Get detailed crate data
169#[utoipa::path(
170    get,
171    path = "/crate_data",
172    tag = "ui",
173    params(CrateDataParams),
174    responses(
175        (status = 200, description = "Crate details", body = CrateData),
176        (status = 404, description = "Crate not found")
177    )
178)]
179pub async fn crate_data(
180    Query(params): Query<CrateDataParams>,
181    State(db): DbState,
182) -> Result<Json<CrateData>, StatusCode> {
183    let index_name = NormalizedName::from(params.name);
184    match db.get_crate_data(&index_name).await {
185        Ok(cd) => Ok(Json(cd)),
186        Err(e) => match e {
187            DbError::CrateNotFound(_) => Err(StatusCode::NOT_FOUND),
188            _ => Err(StatusCode::INTERNAL_SERVER_ERROR),
189        },
190    }
191}
192
193#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema, utoipa::IntoParams)]
194pub struct CratesIoDataParams {
195    name: OriginalName,
196}
197
198/// Get crate data from crates.io
199#[utoipa::path(
200    get,
201    path = "/cratesio_data",
202    tag = "ui",
203    params(CratesIoDataParams),
204    responses(
205        (status = 200, description = "Crates.io crate data", body = String),
206        (status = 404, description = "Crate not found")
207    )
208)]
209pub async fn cratesio_data(Query(params): Query<CratesIoDataParams>) -> Result<String, StatusCode> {
210    let url = format!("https://crates.io/api/v1/crates/{}", params.name);
211
212    let client = reqwest::Client::new();
213    let req = client
214        .get(&url)
215        .header("User-Agent", "kellnr")
216        .header("Accept", "application/json");
217    let resp = req.send().await;
218
219    match resp {
220        Ok(resp) => match resp.status() {
221            StatusCode::OK => {
222                let data = resp.text().await;
223                match data {
224                    Ok(data) => Ok(data),
225                    Err(e) => {
226                        error!("Failed to parse crates.io data: {e}");
227                        Err(StatusCode::INTERNAL_SERVER_ERROR)
228                    }
229                }
230            }
231            StatusCode::NOT_FOUND => Err(StatusCode::NOT_FOUND),
232            _ => {
233                error!("Failed to get crates.io data: {}", resp.status());
234                Err(StatusCode::NOT_FOUND)
235            }
236        },
237        Err(e) => {
238            error!("Failed to get crates.io data: {e}");
239            Err(StatusCode::INTERNAL_SERVER_ERROR)
240        }
241    }
242}
243
244#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)]
245pub struct DeleteCrateVersionParams {
246    name: OriginalName,
247    version: Version,
248}
249
250#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)]
251pub struct DeleteCrateParams {
252    name: OriginalName,
253}
254
255/// Helper function to delete crate versions from db, storage, and docs.
256/// If `versions` is `None`, all versions of the crate are deleted.
257async fn delete_crate_versions_impl(
258    state: &kellnr_appstate::AppStateData,
259    name: &OriginalName,
260    versions: Option<Vec<Version>>,
261) -> Result<(), RouteError> {
262    let versions_to_delete = if let Some(v) = versions {
263        v
264    } else {
265        let crate_meta = state.db.get_crate_meta_list(&name.to_normalized()).await?;
266        crate_meta
267            .iter()
268            .map(|cm| Version::from_unchecked_str(&cm.version))
269            .collect()
270    };
271
272    for version in &versions_to_delete {
273        if let Err(e) = state.db.delete_crate(&name.to_normalized(), version).await {
274            error!("Failed to delete crate from database: {e:?}");
275            return Err(RouteError::Status(StatusCode::INTERNAL_SERVER_ERROR));
276        }
277
278        if let Err(e) = state.crate_storage.delete(name, version).await {
279            error!("Failed to delete crate from storage: {e}");
280            return Err(RouteError::Status(StatusCode::INTERNAL_SERVER_ERROR));
281        }
282
283        if let Err(e) = kellnr_docs::delete(name, version, &state.settings).await {
284            error!("Failed to delete crate from docs: {e}");
285            return Err(RouteError::Status(StatusCode::INTERNAL_SERVER_ERROR));
286        }
287    }
288
289    Ok(())
290}
291
292pub async fn delete_version(
293    Query(params): Query<DeleteCrateVersionParams>,
294    _user: AdminUser,
295    State(state): AppState,
296) -> Result<(), RouteError> {
297    delete_crate_versions_impl(&state, &params.name, Some(vec![params.version])).await
298}
299
300pub async fn delete_crate(
301    Query(params): Query<DeleteCrateParams>,
302    _user: AdminUser,
303    State(state): AppState,
304) -> Result<(), RouteError> {
305    delete_crate_versions_impl(&state, &params.name, None).await
306}
307
308/// Delete a specific version of a crate (path parameter version)
309#[utoipa::path(
310    delete,
311    path = "/crates/{name}/{version}",
312    tag = "ui",
313    params(
314        ("name" = String, Path, description = "Crate name"),
315        ("version" = String, Path, description = "Version to delete")
316    ),
317    responses(
318        (status = 200, description = "Crate version deleted"),
319        (status = 400, description = "Invalid parameters"),
320        (status = 403, description = "Admin access required")
321    ),
322    security(("session_cookie" = []))
323)]
324pub async fn delete_crate_version(
325    Path((name, version)): Path<(String, String)>,
326    _user: AdminUser,
327    State(state): AppState,
328) -> Result<(), RouteError> {
329    let name = OriginalName::try_from(name.as_str())
330        .map_err(|_| RouteError::Status(StatusCode::BAD_REQUEST))?;
331    let version = Version::try_from(version.as_str())
332        .map_err(|_| RouteError::Status(StatusCode::BAD_REQUEST))?;
333
334    delete_crate_versions_impl(&state, &name, Some(vec![version])).await
335}
336
337/// Delete all versions of a crate (path parameter version)
338#[utoipa::path(
339    delete,
340    path = "/crates/{name}",
341    tag = "ui",
342    params(
343        ("name" = String, Path, description = "Crate name")
344    ),
345    responses(
346        (status = 200, description = "All crate versions deleted"),
347        (status = 400, description = "Invalid parameters"),
348        (status = 403, description = "Admin access required")
349    ),
350    security(("session_cookie" = []))
351)]
352pub async fn delete_crate_all(
353    Path(name): Path<String>,
354    _user: AdminUser,
355    State(state): AppState,
356) -> Result<(), RouteError> {
357    let name = OriginalName::try_from(name.as_str())
358        .map_err(|_| RouteError::Status(StatusCode::BAD_REQUEST))?;
359
360    delete_crate_versions_impl(&state, &name, None).await
361}
362
363#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, ToSchema)]
364pub struct Statistic {
365    pub num_crates: u64,
366    pub num_crate_versions: u64,
367    pub num_crate_downloads: u64,
368    pub num_proxy_crates: u64,
369    pub num_proxy_crate_versions: u64,
370    pub num_proxy_crate_downloads: u64,
371    pub top_crates: TopCrates,
372    pub last_updated_crate: Option<(OriginalName, Version)>,
373    pub proxy_enabled: bool,
374}
375
376#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, ToSchema)]
377pub struct TopCrates {
378    pub first: (String, u64),
379    pub second: (String, u64),
380    pub third: (String, u64),
381}
382
383/// Get registry statistics
384#[utoipa::path(
385    get,
386    path = "/statistics",
387    tag = "ui",
388    responses(
389        (status = 200, description = "Registry statistics", body = Statistic)
390    )
391)]
392pub async fn statistic(State(db): DbState, State(settings): SettingsState) -> Json<Statistic> {
393    fn extract(tops: &[(String, u64)], i: usize) -> (String, u64) {
394        if tops.len() > i {
395            tops[i].clone()
396        } else {
397            (String::new(), 0)
398        }
399    }
400
401    let num_crates = db.get_total_unique_crates().await.unwrap_or_default();
402    let num_crate_versions = db.get_total_crate_versions().await.unwrap_or_default();
403    let num_crate_downloads = db.get_total_downloads().await.unwrap_or_default();
404    let tops = db.get_top_crates_downloads(3).await.unwrap_or_default();
405    let num_proxy_crates = db
406        .get_total_unique_cached_crates()
407        .await
408        .unwrap_or_default();
409    let num_proxy_crate_versions = db
410        .get_total_cached_crate_versions()
411        .await
412        .unwrap_or_default();
413    let num_proxy_crate_downloads = db.get_total_cached_downloads().await.unwrap_or_default();
414    let last_updated_crate = db.get_last_updated_crate().await.unwrap_or_default();
415
416    Json(Statistic {
417        num_crates,
418        num_crate_versions,
419        num_crate_downloads,
420        num_proxy_crates,
421        num_proxy_crate_versions,
422        num_proxy_crate_downloads,
423        top_crates: TopCrates {
424            first: extract(&tops, 0),
425            second: extract(&tops, 1),
426            third: extract(&tops, 2),
427        },
428        last_updated_crate,
429        proxy_enabled: settings.proxy.enabled,
430    })
431}
432
433/// Parameters for triggering a documentation build
434#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema, utoipa::IntoParams)]
435pub struct BuildParams {
436    /// Package name
437    package: OriginalName,
438    /// Package version
439    version: Version,
440}
441
442/// Trigger documentation build for a crate
443///
444/// Add a crate version to the documentation build queue.
445/// Requires ownership of the crate or admin access.
446#[utoipa::path(
447    post,
448    path = "/builds",
449    tag = "docs",
450    params(BuildParams),
451    responses(
452        (status = 200, description = "Build queued successfully"),
453        (status = 400, description = "Crate or version does not exist"),
454        (status = 401, description = "Not authorized or not an owner")
455    ),
456    security(("session_cookie" = []))
457)]
458pub async fn build_rustdoc(
459    Query(params): Query<BuildParams>,
460    State(state): AppState,
461    user: MaybeUser,
462) -> Result<(), StatusCode> {
463    if !state.settings.docs.enabled {
464        return Err(StatusCode::BAD_REQUEST);
465    }
466
467    let normalized_name = NormalizedName::from(params.package);
468    let db = state.db;
469    let version = params.version;
470
471    // Check if crate with the version exists.
472    if let Some(id) = db
473        .get_crate_id(&normalized_name)
474        .await
475        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
476    {
477        if !db
478            .crate_version_exists(id, &version)
479            .await
480            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
481        {
482            return Err(StatusCode::BAD_REQUEST);
483        }
484    } else {
485        return Err(StatusCode::BAD_REQUEST);
486    }
487
488    // If the user is the owner of the crate or any admin user,
489    // the build operation is allowed.
490    let is_allowed = match user {
491        MaybeUser::Normal(user) => db
492            .is_owner(&normalized_name, &user)
493            .await
494            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
495        MaybeUser::Admin(_) => true,
496    };
497
498    if !is_allowed {
499        return Err(StatusCode::UNAUTHORIZED);
500    }
501
502    // Add to build queue
503    db.add_doc_queue(
504        &normalized_name,
505        &version,
506        &state
507            .crate_storage
508            .create_rand_doc_queue_path()
509            .await
510            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
511    )
512    .await
513    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
514
515    Ok(())
516}
517
518#[cfg(test)]
519mod tests {
520    use std::collections::BTreeMap;
521    use std::sync::Arc;
522
523    use axum::Router;
524    use axum::body::Body;
525    use axum::routing::{get, post};
526    use axum_extra::extract::cookie::Key;
527    use http_body_util::BodyExt;
528    use hyper::{Request, header};
529    use kellnr_appstate::AppStateData;
530    use kellnr_common::crate_data::{CrateRegistryDep, CrateVersionData};
531    use kellnr_db::User;
532    use kellnr_db::error::DbError;
533    use kellnr_db::mock::MockDb;
534    use kellnr_settings::{Postgresql, Settings, constants};
535    use kellnr_storage::cached_crate_storage::DynStorage;
536    use kellnr_storage::fs_storage::FSStorage;
537    use kellnr_storage::kellnr_crate_storage::KellnrCrateStorage;
538    use mockall::predicate::*;
539    use tower::ServiceExt;
540
541    use super::*;
542    use crate::test_helper::encode_cookies;
543
544    #[tokio::test]
545    async fn settings_no_admin_returns_unauthorized() {
546        let mut mock_db = MockDb::new();
547        mock_db
548            .expect_validate_session()
549            .returning(|_| Ok(("admin".to_string(), true)));
550
551        let (settings, storage) = test_deps();
552        let r = app(
553            mock_db,
554            KellnrCrateStorage::new(&settings, storage),
555            settings,
556        )
557        .oneshot(Request::get("/settings").body(Body::empty()).unwrap())
558        .await
559        .unwrap();
560
561        assert_eq!(r.status(), StatusCode::UNAUTHORIZED);
562    }
563
564    #[tokio::test]
565    async fn settings_returns_from_settings() {
566        let mut mock_db = MockDb::new();
567        mock_db
568            .expect_validate_session()
569            .returning(|_| Ok(("admin".to_string(), true)));
570
571        let (settings, storage) = test_deps();
572        let r = app(
573            mock_db,
574            KellnrCrateStorage::new(&settings, storage),
575            settings,
576        )
577        .oneshot(
578            Request::get("/settings")
579                .header(
580                    header::COOKIE,
581                    encode_cookies([(constants::COOKIE_SESSION_ID, "cookie")]),
582                )
583                .header(header::CONTENT_TYPE, "application/json")
584                .header(header::AUTHORIZATION, "token")
585                .body(Body::empty())
586                .unwrap(),
587        )
588        .await
589        .unwrap();
590
591        let result_status = r.status();
592        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
593        let result_response = serde_json::from_slice::<SettingsResponse>(&result_msg).unwrap();
594
595        // Set the password to empty string because it is not serialized
596        let tmp = kellnr_settings::test_settings();
597        let psq = Postgresql {
598            pwd: String::default(),
599            ..tmp.postgresql
600        };
601        let expected_state = Settings {
602            postgresql: psq,
603            ..tmp
604        };
605
606        assert_eq!(result_status, StatusCode::OK);
607        assert_eq!(result_response.settings, expected_state);
608        // Verify that sources are present in the response
609        assert!(result_response.sources.contains_key("registry.data_dir"));
610    }
611
612    #[tokio::test]
613    async fn docs_enabled_no_auth_returns_ok() {
614        let mock_db = MockDb::new();
615        let (settings, storage) = test_deps();
616        let r = app(
617            mock_db,
618            KellnrCrateStorage::new(&settings, storage),
619            settings,
620        )
621        .oneshot(Request::get("/docs_enabled").body(Body::empty()).unwrap())
622        .await
623        .unwrap();
624
625        assert_eq!(r.status(), StatusCode::OK);
626    }
627
628    #[tokio::test]
629    async fn docs_enabled_returns_false_by_default() {
630        let mock_db = MockDb::new();
631        let (settings, storage) = test_deps();
632        let r = app(
633            mock_db,
634            KellnrCrateStorage::new(&settings, storage),
635            settings,
636        )
637        .oneshot(Request::get("/docs_enabled").body(Body::empty()).unwrap())
638        .await
639        .unwrap();
640
641        let result_status = r.status();
642        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
643        let result = serde_json::from_slice::<DocsEnabledResponse>(&result_msg).unwrap();
644
645        assert_eq!(StatusCode::OK, result_status);
646        assert!(!result.enabled);
647    }
648
649    #[tokio::test]
650    async fn docs_enabled_returns_true_when_enabled() {
651        let mock_db = MockDb::new();
652        let (mut settings, storage) = test_deps();
653        settings.docs.enabled = true;
654        let r = app(
655            mock_db,
656            KellnrCrateStorage::new(&settings, storage),
657            settings,
658        )
659        .oneshot(Request::get("/docs_enabled").body(Body::empty()).unwrap())
660        .await
661        .unwrap();
662
663        let result_status = r.status();
664        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
665        let result = serde_json::from_slice::<DocsEnabledResponse>(&result_msg).unwrap();
666
667        assert_eq!(StatusCode::OK, result_status);
668        assert!(result.enabled);
669    }
670
671    #[tokio::test]
672    async fn build_rust_doc_crate_not_found() {
673        let mut mock_db = MockDb::new();
674        mock_db
675            .expect_get_crate_id()
676            .with(eq(NormalizedName::from_unchecked("foobar".to_string())))
677            .returning(move |_| Ok(None));
678        mock_db
679            .expect_validate_session()
680            .with(eq("cookie"))
681            .returning(move |_| Ok(("user".to_string(), false)));
682        let (mut settings, storage) = test_deps();
683        settings.docs.enabled = true;
684        let r = app(
685            mock_db,
686            KellnrCrateStorage::new(&settings, storage),
687            settings,
688        )
689        .oneshot(
690            Request::post("/build?package=foobar&version=1.0.0")
691                .header(
692                    header::COOKIE,
693                    encode_cookies([(constants::COOKIE_SESSION_ID, "cookie")]),
694                )
695                .header(header::CONTENT_TYPE, "application/json")
696                .header(header::AUTHORIZATION, "token")
697                .body(Body::empty())
698                .unwrap(),
699        )
700        .await
701        .unwrap();
702
703        assert_eq!(r.status(), StatusCode::BAD_REQUEST);
704    }
705
706    #[tokio::test]
707    async fn build_rust_doc_version_not_found() {
708        let mut mock_db = MockDb::new();
709        mock_db
710            .expect_get_crate_id()
711            .with(eq(NormalizedName::from_unchecked("foobar".to_string())))
712            .returning(move |_| Ok(Some(1)));
713        mock_db
714            .expect_validate_session()
715            .with(eq("cookie"))
716            .returning(move |_| Ok(("user".to_string(), false)));
717        mock_db
718            .expect_crate_version_exists()
719            .with(eq(1), eq("1.0.0"))
720            .returning(move |_, _| Ok(false));
721        let (mut settings, storage) = test_deps();
722        settings.docs.enabled = true;
723        let r = app(
724            mock_db,
725            KellnrCrateStorage::new(&settings, storage),
726            settings,
727        )
728        .oneshot(
729            Request::post("/build?package=foobar&version=1.0.0")
730                .header(
731                    header::COOKIE,
732                    encode_cookies([(constants::COOKIE_SESSION_ID, "cookie")]),
733                )
734                .header(header::CONTENT_TYPE, "application/json")
735                .header(header::AUTHORIZATION, "token")
736                .body(Body::empty())
737                .unwrap(),
738        )
739        .await
740        .unwrap();
741
742        assert_eq!(r.status(), StatusCode::BAD_REQUEST);
743    }
744
745    #[tokio::test]
746    async fn build_rust_doc_not_owner() {
747        let mut mock_db = MockDb::new();
748        mock_db
749            .expect_get_crate_id()
750            .with(eq(NormalizedName::from_unchecked("foobar".to_string())))
751            .returning(move |_| Ok(Some(1)));
752        mock_db
753            .expect_validate_session()
754            .with(eq("cookie"))
755            .returning(move |_| Ok(("user".to_string(), false)));
756        mock_db
757            .expect_crate_version_exists()
758            .with(eq(1), eq("1.0.0"))
759            .returning(move |_, _| Ok(true));
760        mock_db
761            .expect_is_owner()
762            .with(
763                eq(NormalizedName::from_unchecked("foobar".to_string())),
764                eq("user"),
765            )
766            .returning(move |_, _| Ok(false));
767        mock_db
768            .expect_get_user()
769            .with(eq("user"))
770            .returning(move |_| {
771                Ok(User {
772                    id: 0,
773                    name: "user".to_string(),
774                    pwd: String::new(),
775                    salt: String::new(),
776                    is_admin: false,
777                    is_read_only: false,
778                    created: String::new(),
779                })
780            });
781        let (mut settings, storage) = test_deps();
782        settings.docs.enabled = true;
783        let r = app(
784            mock_db,
785            KellnrCrateStorage::new(&settings, storage),
786            settings,
787        )
788        .oneshot(
789            Request::post("/build?package=foobar&version=1.0.0")
790                .header(
791                    header::COOKIE,
792                    encode_cookies([(constants::COOKIE_SESSION_ID, "cookie")]),
793                )
794                .header(header::CONTENT_TYPE, "application/json")
795                .header(header::AUTHORIZATION, "token")
796                .body(Body::empty())
797                .unwrap(),
798        )
799        .await
800        .unwrap();
801
802        assert_eq!(r.status(), StatusCode::UNAUTHORIZED);
803    }
804
805    #[tokio::test]
806    async fn build_rust_doc_is_owner() {
807        let mut mock_db = MockDb::new();
808        mock_db
809            .expect_get_crate_id()
810            .with(eq(NormalizedName::from_unchecked("foobar".to_string())))
811            .returning(move |_| Ok(Some(1)));
812        mock_db
813            .expect_validate_session()
814            .with(eq("cookie"))
815            .returning(move |_| Ok(("user".to_string(), false)));
816        mock_db
817            .expect_crate_version_exists()
818            .with(eq(1), eq("1.0.0"))
819            .returning(move |_, _| Ok(true));
820        mock_db
821            .expect_is_owner()
822            .with(
823                eq(NormalizedName::from_unchecked("foobar".to_string())),
824                eq("user"),
825            )
826            .returning(move |_, _| Ok(true));
827        mock_db
828            .expect_get_user()
829            .with(eq("user"))
830            .returning(move |_| {
831                Ok(User {
832                    id: 0,
833                    name: "user".to_string(),
834                    pwd: String::new(),
835                    salt: String::new(),
836                    is_admin: false,
837                    is_read_only: false,
838                    created: String::new(),
839                })
840            });
841        mock_db
842            .expect_add_doc_queue()
843            .with(
844                eq(NormalizedName::from_unchecked("foobar".to_string())),
845                eq(Version::try_from("1.0.0").unwrap()),
846                always(),
847            )
848            .times(1)
849            .returning(move |_, _, _| Ok(()));
850
851        let (mut settings, storage) = test_deps();
852        settings.docs.enabled = true;
853        let r = app(
854            mock_db,
855            KellnrCrateStorage::new(&settings, storage),
856            settings,
857        )
858        .oneshot(
859            Request::post("/build?package=foobar&version=1.0.0")
860                .header(
861                    header::COOKIE,
862                    encode_cookies([(constants::COOKIE_SESSION_ID, "cookie")]),
863                )
864                .header(header::CONTENT_TYPE, "application/json")
865                .header(header::AUTHORIZATION, "token")
866                .body(Body::empty())
867                .unwrap(),
868        )
869        .await
870        .unwrap();
871
872        assert_eq!(r.status(), StatusCode::OK);
873    }
874
875    #[tokio::test]
876    async fn build_rust_doc_not_owner_but_admin() {
877        let mut mock_db = MockDb::new();
878        mock_db
879            .expect_get_crate_id()
880            .with(eq(NormalizedName::from_unchecked("foobar".to_string())))
881            .returning(move |_| Ok(Some(1)));
882        mock_db
883            .expect_validate_session()
884            .with(eq("cookie"))
885            .returning(move |_| Ok(("user".to_string(), true)));
886        mock_db
887            .expect_crate_version_exists()
888            .with(eq(1), eq("1.0.0"))
889            .returning(move |_, _| Ok(true));
890        mock_db
891            .expect_is_owner()
892            .with(
893                eq(NormalizedName::from_unchecked("foobar".to_string())),
894                eq("user"),
895            )
896            .returning(move |_, _| Ok(false));
897        mock_db
898            .expect_get_user()
899            .with(eq("user"))
900            .returning(move |_| {
901                Ok(User {
902                    id: 0,
903                    name: "user".to_string(),
904                    pwd: String::new(),
905                    salt: String::new(),
906                    is_admin: true,
907                    is_read_only: false,
908                    created: String::new(),
909                })
910            });
911        mock_db
912            .expect_add_doc_queue()
913            .with(
914                eq(NormalizedName::from_unchecked("foobar".to_string())),
915                eq(Version::try_from("1.0.0").unwrap()),
916                always(),
917            )
918            .times(1)
919            .returning(move |_, _, _| Ok(()));
920
921        let (mut settings, storage) = test_deps();
922        settings.docs.enabled = true;
923        let r = app(
924            mock_db,
925            KellnrCrateStorage::new(&settings, storage),
926            settings,
927        )
928        .oneshot(
929            Request::post("/build?package=foobar&version=1.0.0")
930                .header(header::CONTENT_TYPE, "application/json")
931                .header(header::AUTHORIZATION, "token")
932                .header(
933                    header::COOKIE,
934                    encode_cookies([(constants::COOKIE_SESSION_ID, "cookie")]),
935                )
936                .body(Body::empty())
937                .unwrap(),
938        )
939        .await
940        .unwrap();
941
942        assert_eq!(r.status(), StatusCode::OK);
943    }
944
945    #[tokio::test]
946    async fn statistic_returns_sparse_statistics() {
947        let mut mock_db = MockDb::new();
948        mock_db
949            .expect_get_total_unique_crates()
950            .returning(move || Err(DbError::FailedToCountCrates));
951        mock_db
952            .expect_get_total_crate_versions()
953            .returning(move || Err(DbError::FailedToCountCrateVersions));
954        mock_db
955            .expect_get_total_downloads()
956            .returning(move || Err(DbError::FailedToCountTotalDownloads));
957        mock_db
958            .expect_get_top_crates_downloads()
959            .with(eq(3))
960            .returning(move |_| Ok(vec![("top1".to_string(), 1000)]));
961        mock_db
962            .expect_get_last_updated_crate()
963            .returning(move || Ok(None));
964        mock_db
965            .expect_get_total_unique_cached_crates()
966            .returning(move || Err(DbError::FailedToCountCrates));
967        mock_db
968            .expect_get_total_cached_crate_versions()
969            .returning(move || Err(DbError::FailedToCountCrateVersions));
970        mock_db
971            .expect_get_total_cached_downloads()
972            .returning(move || Err(DbError::FailedToCountTotalDownloads));
973
974        let (settings, storage) = test_deps();
975        let r = app(
976            mock_db,
977            KellnrCrateStorage::new(&settings, storage),
978            settings,
979        )
980        .oneshot(Request::get("/statistic").body(Body::empty()).unwrap())
981        .await
982        .unwrap();
983
984        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
985        let result_stat = serde_json::from_slice::<Statistic>(&result_msg).unwrap();
986
987        let expect = Statistic {
988            num_crates: 0,
989            num_crate_versions: 0,
990            num_crate_downloads: 0,
991            num_proxy_crates: 0,
992            num_proxy_crate_versions: 0,
993            num_proxy_crate_downloads: 0,
994            top_crates: TopCrates {
995                first: ("top1".to_string(), 1000),
996                second: (String::new(), 0),
997                third: (String::new(), 0),
998            },
999            last_updated_crate: None,
1000            proxy_enabled: false,
1001        };
1002
1003        assert_eq!(expect, result_stat);
1004    }
1005
1006    #[tokio::test]
1007    async fn statistic_returns_empty_statistics() {
1008        let mut mock_db = MockDb::new();
1009        mock_db
1010            .expect_get_total_unique_crates()
1011            .returning(move || Err(DbError::FailedToCountCrates));
1012        mock_db
1013            .expect_get_total_crate_versions()
1014            .returning(move || Err(DbError::FailedToCountCrateVersions));
1015        mock_db
1016            .expect_get_total_downloads()
1017            .returning(move || Err(DbError::FailedToCountTotalDownloads));
1018        mock_db
1019            .expect_get_top_crates_downloads()
1020            .with(eq(3))
1021            .returning(move |_| Err(DbError::FailedToCountTotalDownloads));
1022        mock_db
1023            .expect_get_last_updated_crate()
1024            .returning(move || Ok(None));
1025        mock_db
1026            .expect_get_total_unique_cached_crates()
1027            .returning(move || Err(DbError::FailedToCountCrates));
1028        mock_db
1029            .expect_get_total_cached_crate_versions()
1030            .returning(move || Err(DbError::FailedToCountCrateVersions));
1031        mock_db
1032            .expect_get_total_cached_downloads()
1033            .returning(move || Err(DbError::FailedToCountTotalDownloads));
1034
1035        let (settings, storage) = test_deps();
1036        let r = app(
1037            mock_db,
1038            KellnrCrateStorage::new(&settings, storage),
1039            settings,
1040        )
1041        .oneshot(Request::get("/statistic").body(Body::empty()).unwrap())
1042        .await
1043        .unwrap();
1044
1045        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
1046        let result_stat = serde_json::from_slice::<Statistic>(&result_msg).unwrap();
1047
1048        let expect = Statistic {
1049            num_crates: 0,
1050            num_crate_versions: 0,
1051            num_crate_downloads: 0,
1052            num_proxy_crates: 0,
1053            num_proxy_crate_versions: 0,
1054            num_proxy_crate_downloads: 0,
1055            top_crates: TopCrates {
1056                first: (String::new(), 0),
1057                second: (String::new(), 0),
1058                third: (String::new(), 0),
1059            },
1060            last_updated_crate: None,
1061            proxy_enabled: false,
1062        };
1063
1064        assert_eq!(expect, result_stat);
1065    }
1066
1067    #[tokio::test]
1068    async fn statistic_returns_crate_statistics() {
1069        let mut mock_db = MockDb::new();
1070        mock_db
1071            .expect_get_total_unique_crates()
1072            .returning(move || Ok(1000));
1073        mock_db
1074            .expect_get_total_crate_versions()
1075            .returning(move || Ok(10000));
1076        mock_db
1077            .expect_get_total_downloads()
1078            .returning(move || Ok(100_000));
1079        mock_db
1080            .expect_get_top_crates_downloads()
1081            .with(eq(3))
1082            .returning(move |_| {
1083                Ok(vec![
1084                    ("top1".to_string(), 1000),
1085                    ("top2".to_string(), 500),
1086                    ("top3".to_string(), 100),
1087                ])
1088            });
1089        mock_db
1090            .expect_get_total_unique_cached_crates()
1091            .returning(move || Ok(9999));
1092        mock_db
1093            .expect_get_total_cached_crate_versions()
1094            .returning(move || Ok(99999));
1095        mock_db
1096            .expect_get_total_cached_downloads()
1097            .returning(move || Ok(999_999));
1098        mock_db.expect_get_last_updated_crate().returning(move || {
1099            Ok(Some((
1100                OriginalName::from_unchecked("foobar".to_string()),
1101                Version::try_from("1.0.0").unwrap(),
1102            )))
1103        });
1104
1105        let (settings, storage) = test_deps();
1106        let r = app(
1107            mock_db,
1108            KellnrCrateStorage::new(&settings, storage),
1109            settings,
1110        )
1111        .oneshot(Request::get("/statistic").body(Body::empty()).unwrap())
1112        .await
1113        .unwrap();
1114
1115        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
1116        let result_stat = serde_json::from_slice::<Statistic>(&result_msg).unwrap();
1117
1118        let expect = Statistic {
1119            num_crates: 1000,
1120            num_crate_versions: 10000,
1121            num_crate_downloads: 100_000,
1122            num_proxy_crates: 9999,
1123            num_proxy_crate_versions: 99999,
1124            num_proxy_crate_downloads: 999_999,
1125            top_crates: TopCrates {
1126                first: ("top1".to_string(), 1000),
1127                second: ("top2".to_string(), 500),
1128                third: ("top3".to_string(), 100),
1129            },
1130            last_updated_crate: Some((
1131                OriginalName::from_unchecked("foobar".to_string()),
1132                Version::try_from("1.0.0").unwrap(),
1133            )),
1134            proxy_enabled: false,
1135        };
1136        assert_eq!(expect, result_stat);
1137    }
1138
1139    #[tokio::test]
1140    async fn kellnr_version_returns_version() {
1141        let (settings, storage) = test_deps();
1142        let mock_db = MockDb::new();
1143
1144        let r = app(
1145            mock_db,
1146            KellnrCrateStorage::new(&settings, storage),
1147            settings,
1148        )
1149        .oneshot(Request::get("/version").body(Body::empty()).unwrap())
1150        .await
1151        .unwrap();
1152
1153        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
1154        let result_version = serde_json::from_slice::<KellnrVersion>(&result_msg).unwrap();
1155
1156        assert_eq!("0.0.0-unknown", result_version.version);
1157    }
1158
1159    #[tokio::test]
1160    async fn search_not_hits_returns_nothing() {
1161        let mut mock_db = MockDb::new();
1162        let (settings, storage) = test_deps();
1163
1164        mock_db
1165            .expect_search_in_crate_name()
1166            .with(eq("doesnotexist"), eq(false))
1167            .returning(move |_name, _| Ok(vec![]));
1168
1169        let r = app(
1170            mock_db,
1171            KellnrCrateStorage::new(&settings, storage),
1172            settings,
1173        )
1174        .oneshot(
1175            Request::get("/search?name=doesnotexist")
1176                .body(Body::empty())
1177                .unwrap(),
1178        )
1179        .await
1180        .unwrap();
1181
1182        let result_status = r.status();
1183        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
1184        let result_crates = serde_json::from_slice::<Pagination>(&result_msg).unwrap();
1185
1186        assert_eq!(StatusCode::OK, result_status);
1187        assert_eq!(0, result_crates.crates.len());
1188        assert_eq!(0, result_crates.page);
1189        assert_eq!(0, result_crates.page_size);
1190    }
1191
1192    #[tokio::test]
1193    async fn search_returns_only_searched_results() {
1194        let mut mock_db = MockDb::new();
1195        let (settings, storage) = test_deps();
1196
1197        let test_crate_summary = CrateOverview {
1198            name: "hello".to_string(),
1199            version: "1.0.0".to_string(),
1200            date: "12-10-2021 05:41:00".to_string(),
1201            total_downloads: 2,
1202            ..Default::default()
1203        };
1204
1205        let tc = test_crate_summary.clone();
1206        mock_db
1207            .expect_search_in_crate_name()
1208            .with(eq("hello"), eq(false))
1209            .returning(move |_, _| Ok(vec![tc.clone()]));
1210
1211        let r = app(
1212            mock_db,
1213            KellnrCrateStorage::new(&settings, storage),
1214            settings,
1215        )
1216        .oneshot(
1217            Request::get("/search?name=hello")
1218                .body(Body::empty())
1219                .unwrap(),
1220        )
1221        .await
1222        .unwrap();
1223
1224        let result_status = r.status();
1225        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
1226        let result_crates = serde_json::from_slice::<Pagination>(&result_msg).unwrap();
1227
1228        assert_eq!(StatusCode::OK, result_status);
1229        assert_eq!(1, result_crates.crates.len());
1230        assert_eq!(0, result_crates.page);
1231        assert_eq!(1, result_crates.page_size);
1232        assert_eq!(test_crate_summary, result_crates.crates[0]);
1233    }
1234
1235    #[tokio::test]
1236    async fn crate_get_crate_information() {
1237        let mut mock_db = MockDb::new();
1238        let (settings, storage) = test_deps();
1239
1240        let expected_crate_data = CrateData {
1241            name: "crate1".to_string(),
1242            owners: vec!["owner1".to_string(), "owner2".to_string()],
1243            max_version: "1.0.0".to_string(),
1244            total_downloads: 5,
1245            last_updated: "12-10-2021 05:41:00".to_string(),
1246            homepage: Some("homepage".to_string()),
1247            description: Some("description".to_string()),
1248            categories: vec!["cat1".to_string(), "cat2".to_string()],
1249            keywords: vec!["key1".to_string(), "key2".to_string()],
1250            authors: vec!["author1".to_string(), "author2".to_string()],
1251            repository: Some("repository".to_string()),
1252            versions: vec![CrateVersionData {
1253                version: "1.0.0".to_string(),
1254                created: "12-10-2021 05:41:00".to_string(),
1255                downloads: 5,
1256                readme: Some("readme".to_string()),
1257                license: Some("MIT".to_string()),
1258                license_file: Some("license".to_string()),
1259                documentation: Some("documentation".to_string()),
1260                dependencies: vec![CrateRegistryDep {
1261                    name: "dep1".to_string(),
1262                    description: Some("description".to_string()),
1263                    version_req: "1.0.0".to_string(),
1264                    target: Some("target".to_string()),
1265                    kind: Some("dev".to_string()),
1266                    registry: Some("registry".to_string()),
1267                    ..Default::default()
1268                }],
1269                checksum: "checksum".to_string(),
1270                features: BTreeMap::default(),
1271                yanked: false,
1272                links: Some("links".to_string()),
1273                v: 1,
1274            }],
1275        };
1276
1277        let ecd = expected_crate_data.clone();
1278        mock_db
1279            .expect_get_crate_data()
1280            .returning(move |_| Ok(ecd.clone()));
1281
1282        let r = app(
1283            mock_db,
1284            KellnrCrateStorage::new(&settings, storage),
1285            settings,
1286        )
1287        .oneshot(
1288            Request::get("/crate_data?name=crate1&version=1.0.0")
1289                .body(Body::empty())
1290                .unwrap(),
1291        )
1292        .await
1293        .unwrap();
1294
1295        let result_status = r.status();
1296        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
1297        let result_crate_data = serde_json::from_slice::<CrateData>(&result_msg).unwrap();
1298
1299        assert_eq!(StatusCode::OK, result_status);
1300        assert_eq!(expected_crate_data, result_crate_data);
1301    }
1302
1303    #[tokio::test]
1304    async fn crates_get_page() {
1305        let mut mock_db = MockDb::new();
1306        let (settings, storage) = test_deps();
1307
1308        let test_crate_overview = CrateOverview {
1309            name: "c1".to_string(),
1310            version: "1.0.0".to_string(),
1311            description: None,
1312            total_downloads: 2,
1313            date: "12-10-2021 05:41:00".to_string(),
1314            documentation: None,
1315            is_cache: false,
1316        };
1317
1318        let test_crates = std::iter::repeat_with(|| test_crate_overview.clone())
1319            .take(10)
1320            .collect::<Vec<_>>();
1321
1322        let tc = test_crates.clone();
1323
1324        mock_db
1325            .expect_get_crate_overview_list()
1326            .with(eq(10), eq(0), eq(false))
1327            .returning(move |_, _, _| Ok(tc.clone()));
1328
1329        let r = app(
1330            mock_db,
1331            KellnrCrateStorage::new(&settings, storage),
1332            settings,
1333        )
1334        .oneshot(Request::get("/crates?page=0").body(Body::empty()).unwrap())
1335        .await
1336        .unwrap();
1337
1338        let result_status = r.status();
1339        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
1340        let result_pagination = serde_json::from_slice::<Pagination>(&result_msg).unwrap();
1341
1342        let expected = test_crates[0..10].to_vec();
1343        assert_eq!(StatusCode::OK, result_status);
1344        assert_eq!(0, result_pagination.page);
1345        assert_eq!(10, result_pagination.page_size);
1346        assert_eq!(10, result_pagination.crates.len());
1347        assert_eq!(expected, result_pagination.crates);
1348    }
1349
1350    #[tokio::test]
1351    async fn crates_get_all_crates() {
1352        let mut mock_db = MockDb::new();
1353        let (settings, storage) = test_deps();
1354
1355        let expected_crate_overview = vec![
1356            CrateOverview {
1357                name: "c1".to_string(),
1358                version: "1.0.0".to_string(),
1359                date: "12-11-2021 05:41:00".to_string(),
1360                total_downloads: 1,
1361                description: Some("Desc".to_string()),
1362                documentation: Some("Docs".to_string()),
1363                is_cache: true,
1364            },
1365            CrateOverview {
1366                name: "c2".to_string(),
1367                version: "2.0.0".to_string(),
1368                date: "12-12-2021 05:41:00".to_string(),
1369                total_downloads: 2,
1370                description: Some("Desc".to_string()),
1371                documentation: Some("Docs".to_string()),
1372                is_cache: true,
1373            },
1374            CrateOverview {
1375                name: "c3".to_string(),
1376                version: "3.0.0".to_string(),
1377                date: "12-09-2021 05:41:00".to_string(),
1378                total_downloads: 3,
1379                description: None,
1380                documentation: None,
1381                is_cache: true,
1382            },
1383        ];
1384
1385        let crate_overview = expected_crate_overview.clone();
1386        mock_db
1387            .expect_get_crate_overview_list()
1388            .with(eq(10), eq(0), eq(false))
1389            .returning(move |_, _, _| Ok(crate_overview.clone()));
1390
1391        let r = app(
1392            mock_db,
1393            KellnrCrateStorage::new(&settings, storage),
1394            settings,
1395        )
1396        .oneshot(Request::get("/crates").body(Body::empty()).unwrap())
1397        .await
1398        .unwrap();
1399
1400        let result_status = r.status();
1401        let result_msg = r.into_body().collect().await.unwrap().to_bytes();
1402        let result_pagination = serde_json::from_slice::<Pagination>(&result_msg).unwrap();
1403
1404        assert_eq!(StatusCode::OK, result_status);
1405        assert_eq!(3, result_pagination.crates.len());
1406        assert_eq!(0, result_pagination.page);
1407        assert_eq!(10, result_pagination.page_size);
1408        assert_eq!(expected_crate_overview, result_pagination.crates);
1409    }
1410
1411    #[tokio::test]
1412    async fn cratesio_data_returns_data() {
1413        let mock_db = MockDb::new();
1414        let (settings, storage) = test_deps();
1415        let r = app(
1416            mock_db,
1417            KellnrCrateStorage::new(&settings, storage),
1418            settings,
1419        )
1420        .oneshot(
1421            Request::get("/cratesio_data?name=quote")
1422                .body(Body::empty())
1423                .unwrap(),
1424        )
1425        .await
1426        .unwrap();
1427
1428        let result_status = r.status();
1429        let body =
1430            String::from_utf8(r.into_body().collect().await.unwrap().to_bytes().to_vec()).unwrap();
1431        assert!(body.contains("quote"));
1432        assert_eq!(StatusCode::OK, result_status);
1433    }
1434
1435    #[tokio::test]
1436    async fn cratesio_data_not_found() {
1437        let mock_db = MockDb::new();
1438        let (settings, storage) = test_deps();
1439        let r = app(
1440            mock_db,
1441            KellnrCrateStorage::new(&settings, storage),
1442            settings,
1443        )
1444        .oneshot(
1445            Request::get("/cratesio_data?name=thisdoesnotevenexist")
1446                .body(Body::empty())
1447                .unwrap(),
1448        )
1449        .await
1450        .unwrap();
1451
1452        assert_eq!(r.status(), StatusCode::NOT_FOUND);
1453    }
1454
1455    fn test_deps() -> (Settings, DynStorage) {
1456        let settings = kellnr_settings::test_settings();
1457        let storage = FSStorage::new(&settings.crates_path()).unwrap();
1458        let storage = Box::new(storage) as DynStorage;
1459        (settings, storage)
1460    }
1461
1462    const TEST_KEY: &[u8] = &[1; 64];
1463    fn app(mock_db: MockDb, crate_storage: KellnrCrateStorage, settings: Settings) -> Router {
1464        Router::new()
1465            .route("/search", get(search))
1466            .route("/crates", get(crates))
1467            .route("/crate_data", get(crate_data))
1468            .route("/version", get(kellnr_version))
1469            .route("/statistic", get(statistic))
1470            .route("/build", post(build_rustdoc))
1471            .route("/cratesio_data", get(cratesio_data))
1472            .route("/settings", get(crate::ui::settings))
1473            .route("/docs_enabled", get(docs_enabled))
1474            .with_state(AppStateData {
1475                db: Arc::new(mock_db),
1476                signing_key: Key::from(TEST_KEY),
1477                settings: Arc::new(settings),
1478                crate_storage: Arc::new(crate_storage),
1479                ..kellnr_appstate::test_state()
1480            })
1481    }
1482}