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#[derive(Serialize, Deserialize)]
23pub struct SettingsResponse {
24 #[serde(flatten)]
25 pub settings: Settings,
26 pub sources: SourceMap,
27 pub defaults: Settings,
28}
29
30#[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)] pub 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#[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)] pub 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#[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)] pub 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#[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#[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(¶ms.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, crates,
160 })
161}
162
163#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema, utoipa::IntoParams)]
164pub struct CrateDataParams {
165 name: OriginalName,
166}
167
168#[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#[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
255async 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, ¶ms.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, ¶ms.name, None).await
306}
307
308#[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#[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#[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#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema, utoipa::IntoParams)]
435pub struct BuildParams {
436 package: OriginalName,
438 version: Version,
440}
441
442#[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 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 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 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 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 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}