1use std::sync::Arc;
2
3use axum::Json;
4use axum::extract::{Path, State};
5use axum::response::Redirect;
6use kellnr_common::binary_body::BinaryBody;
7use kellnr_common::canonical_name::CanonicalName;
8use kellnr_common::original_name::OriginalName;
9use kellnr_common::version::Version;
10use kellnr_db::DbProvider;
11use kellnr_http::authz::{can_read_crate, require_owner, require_writable};
12use kellnr_http::error::ApiResult;
13use kellnr_http::identity::{AnyUser, Identity, TokenUser};
14use kellnr_http::state::{AppState, DbState, DocsStorageState, SettingsState};
15use kellnr_storage::docs_storage::DocsName;
16use tracing::error;
17
18use crate::doc_archive::DocArchive;
19use crate::doc_queue_response::DocQueueResponse;
20use crate::docs_error::DocsError;
21use crate::upload::upload_dir_and_prune;
22use crate::upload_response::DocUploadResponse;
23use crate::{compute_doc_url, doc_exists};
24
25#[utoipa::path(
30 get,
31 path = "/builds",
32 tag = "docs",
33 responses(
34 (status = 200, description = "Documentation build queue", body = DocQueueResponse)
35 ),
36 security(("session_cookie" = []))
37)]
38pub async fn docs_in_queue(
39 user: Option<AnyUser>,
40 State(db): DbState,
41) -> ApiResult<Json<DocQueueResponse>> {
42 let mut visible = Vec::new();
43 for entry in db.get_doc_queue().await? {
44 let Ok(name) = entry.canonical_name() else {
45 continue;
46 };
47 if can_read_docs(&name, user.as_deref(), &db).await {
48 visible.push(entry);
49 }
50 }
51 Ok(Json(DocQueueResponse::from(visible)))
52}
53
54#[utoipa::path(
58 get,
59 path = "/{package}/latest",
60 tag = "docs",
61 params(
62 ("package" = String, Path, description = "Package name")
63 ),
64 responses(
65 (status = 302, description = "Redirect to latest documentation")
66 ),
67 security(("session_cookie" = []))
68)]
69pub async fn latest_docs(
70 Path(package): Path<OriginalName>,
71 user: Option<AnyUser>,
72 State(settings): SettingsState,
73 State(docs_storage): DocsStorageState,
74 State(db): DbState,
75) -> Redirect {
76 let name = package.to_canonical();
77
78 if !can_read_docs(&name, user.as_deref(), &db).await {
80 return Redirect::temporary("/");
81 }
82
83 if let Ok(version) = db.get_max_version_from_name(&name).await
84 && let Ok(Some(original_name)) = db.get_original_name(&name, &version).await
85 && doc_exists(&original_name, &version, &docs_storage).await
86 {
87 return Redirect::temporary(&compute_doc_url(
88 &original_name,
89 &version,
90 &settings.origin.path_prefix(),
91 ));
92 }
93
94 Redirect::temporary("/")
95}
96
97async fn can_read_docs(
99 crate_name: &CanonicalName,
100 user: Option<&Identity>,
101 db: &Arc<dyn DbProvider>,
102) -> bool {
103 can_read_crate(crate_name, user, db)
104 .await
105 .unwrap_or_else(|e| {
106 error!("Failed to check docs access for {crate_name}: {e}");
107 false
108 })
109}
110
111#[utoipa::path(
116 put,
117 path = "/{package}/{version}",
118 tag = "docs",
119 params(
120 ("package" = String, Path, description = "Package name"),
121 ("version" = String, Path, description = "Package version")
122 ),
123 request_body(content = inline(BinaryBody), description = "Documentation archive (tar.gz or zip)", content_type = "application/octet-stream"),
124 responses(
125 (status = 200, description = "Documentation published successfully", body = DocUploadResponse),
126 (status = 400, description = "Crate or version does not exist"),
127 (status = 401, description = "Not authorized"),
128 (status = 403, description = "Not an owner of the crate")
129 ),
130 security(("cargo_token" = []))
131)]
132pub async fn publish_docs(
133 Path((package, version)): Path<(OriginalName, Version)>,
134 user: TokenUser,
135 State(state): AppState,
136 mut docs: DocArchive,
137) -> ApiResult<Json<DocUploadResponse>> {
138 let db = state.db;
139 let settings = state.settings;
140 let docs_storage = state.docs_storage;
141 let canonical_name = package.to_canonical();
142
143 require_writable(&user)?;
148 require_owner(&canonical_name, &user, &db).await?;
149
150 let Some(original_name) = db.get_original_name(&canonical_name, &version).await? else {
151 return crate_does_not_exist(&package, &version);
152 };
153
154 let tmp_dir = tempfile::tempdir().map_err(DocsError::IoError)?;
155 let extract_path = tmp_dir.path().to_path_buf();
156
157 tokio::task::spawn_blocking(move || docs.extract(&extract_path))
158 .await
159 .map_err(|_| DocsError::ExtractFailed)??;
160
161 upload_dir_and_prune(
162 tmp_dir.path(),
163 "",
164 &DocsName::from(&original_name),
165 &version,
166 &docs_storage,
167 )
168 .await?;
169
170 db.update_docs_link(
171 &canonical_name,
172 &version,
173 &compute_doc_url(&original_name, &version, &settings.origin.path_prefix()),
174 )
175 .await?;
176
177 Ok(Json(DocUploadResponse::new(
178 "Successfully published docs.".to_string(),
179 &original_name,
180 &version,
181 &settings.origin.path_prefix(),
182 )))
183}
184
185fn crate_does_not_exist(
186 crate_name: &OriginalName,
187 crate_version: &Version,
188) -> ApiResult<Json<DocUploadResponse>> {
189 Err(DocsError::CrateDoesNotExist(crate_name.to_lowercase(), crate_version.to_string()).into())
190}
191
192#[cfg(test)]
193mod tests {
194 use std::io::Write;
195 use std::path::PathBuf;
196 use std::sync::Arc;
197
198 use axum::Router;
199 use axum::body::Body;
200 use axum::http::{Request, StatusCode, header};
201 use axum::routing::{get, put};
202 use http_body_util::BodyExt;
203 use kellnr_common::account_name::{GroupName, UserName};
204 use kellnr_common::canonical_name::CanonicalName;
205 use kellnr_common::plain_token::PlainToken;
206 use kellnr_db::UserFlags;
207 use kellnr_db::test_utils::{TestCrateBuilder, test_drop_table};
208 use kellnr_http::state::AppStateData;
209 use kellnr_http::test_support::sqlite_test_state;
210 use kellnr_storage::cached_crate_storage::DynStorage;
211 use kellnr_storage::docs_storage::DocsStorage;
212 use kellnr_storage::fs_storage::FSStorage;
213 use kellnr_storage::object_storage::MemoryStorage;
214 use kellnr_test_db::TestDb;
215 use tower::ServiceExt;
216
217 use super::*;
218 use crate::doc_queue_response::DocQueueEntryResponse;
219
220 const ADMIN_TOKEN: &str = "token";
222
223 fn app(state: AppStateData, docs_storage: Arc<DocsStorage>) -> Router {
225 Router::new()
226 .route("/builds", get(docs_in_queue))
227 .route("/{package}/latest", get(latest_docs))
228 .route("/{package}/{version}", put(publish_docs))
229 .with_state(AppStateData {
230 docs_storage,
231 ..state
232 })
233 }
234
235 fn memory_docs_storage() -> Arc<DocsStorage> {
236 Arc::new(DocsStorage::new(
237 Box::new(MemoryStorage::new()) as DynStorage
238 ))
239 }
240
241 fn canonical(name: &str) -> CanonicalName {
242 CanonicalName::try_from(name).unwrap()
243 }
244
245 fn user_name(name: &str) -> UserName {
246 UserName::new(name).unwrap()
247 }
248
249 async fn add_crate(db: &TestDb, name: &str, version: &str) {
250 TestCrateBuilder::new(db)
251 .name(name)
252 .version(version)
253 .build()
254 .await
255 .unwrap();
256 }
257
258 async fn add_restricted_crate(db: &TestDb, name: &str, restricted: bool) {
260 add_crate(db, name, "1.0.0").await;
261 db.change_download_restricted(&canonical(name), restricted)
262 .await
263 .unwrap();
264 }
265
266 async fn token_user(db: &TestDb, name: &str, flags: UserFlags) -> String {
268 db.add_user(&user_name(name), "pwd", flags).await.unwrap();
269 let token = format!("{name}-token");
270 db.add_auth_token("test", &PlainToken::from(token.as_str()), &user_name(name))
271 .await
272 .unwrap();
273 token
274 }
275
276 async fn add_doc_queue(db: &TestDb, name: &str, version: &str) {
277 db.add_doc_queue(
278 &OriginalName::try_from(name).unwrap(),
279 &Version::try_from(version).unwrap(),
280 &PathBuf::default(),
281 )
282 .await
283 .unwrap();
284 }
285
286 async fn docs_link(db: &TestDb, name: &str) -> Option<String> {
287 let data = db.get_crate_data(&canonical(name)).await.unwrap();
288 data.versions[0].documentation.clone()
289 }
290
291 #[tokio::test]
292 async fn doc_in_queue_returns_queue_entries() {
293 let (state, db) = sqlite_test_state().await;
294 add_crate(&db, "crate1", "0.0.1").await;
295 add_crate(&db, "crate2", "0.0.2").await;
296 add_doc_queue(&db, "crate1", "0.0.1").await;
297 add_doc_queue(&db, "crate2", "0.0.2").await;
298
299 let r = app(state, memory_docs_storage())
300 .oneshot(Request::get("/builds").body(Body::empty()).unwrap())
301 .await
302 .unwrap();
303
304 let actual = r.into_body().collect().await.unwrap().to_bytes();
305 let actual = serde_json::from_slice::<DocQueueResponse>(&actual).unwrap();
306 assert_eq!(
307 DocQueueResponse {
308 queue: vec![
309 DocQueueEntryResponse {
310 name: "crate1".to_string(),
311 version: "0.0.1".to_string()
312 },
313 DocQueueEntryResponse {
314 name: "crate2".to_string(),
315 version: "0.0.2".to_string()
316 }
317 ]
318 },
319 actual
320 );
321 }
322
323 async fn state_with_restricted_queue_entry() -> (AppStateData, TestDb) {
326 let (state, db) = sqlite_test_state().await;
327 add_restricted_crate(&db, "public", false).await;
328 add_restricted_crate(&db, "secret", true).await;
329 add_doc_queue(&db, "public", "1.0.0").await;
330 add_doc_queue(&db, "secret", "1.0.0").await;
331 (state, db)
332 }
333
334 async fn queued_names(state: AppStateData, token: Option<&str>) -> Vec<String> {
335 let mut request = Request::get("/builds");
336 if let Some(token) = token {
337 request = request.header(header::AUTHORIZATION, token);
338 }
339 let r = app(state, memory_docs_storage())
340 .oneshot(request.body(Body::empty()).unwrap())
341 .await
342 .unwrap();
343 let body = r.into_body().collect().await.unwrap().to_bytes();
344 serde_json::from_slice::<DocQueueResponse>(&body)
345 .unwrap()
346 .queue
347 .into_iter()
348 .map(|e| e.name)
349 .collect()
350 }
351
352 #[tokio::test]
353 async fn doc_queue_hides_restricted_crate_from_anonymous_user() {
354 let (state, _db) = state_with_restricted_queue_entry().await;
355
356 let names = queued_names(state, None).await;
357
358 assert_eq!(names, vec!["public"]);
359 }
360
361 #[tokio::test]
362 async fn doc_queue_hides_restricted_crate_from_user_without_access() {
363 let (state, db) = state_with_restricted_queue_entry().await;
364 let token = token_user(&db, "bob", UserFlags::normal()).await;
365
366 let names = queued_names(state, Some(&token)).await;
367
368 assert_eq!(names, vec!["public"]);
369 }
370
371 #[tokio::test]
372 async fn doc_queue_lists_restricted_crate_for_crate_user() {
373 let (state, db) = state_with_restricted_queue_entry().await;
374 let token = token_user(&db, "alice", UserFlags::normal()).await;
375 db.add_crate_user(&canonical("secret"), &user_name("alice"))
376 .await
377 .unwrap();
378
379 let names = queued_names(state, Some(&token)).await;
380
381 assert_eq!(names, vec!["public", "secret"]);
382 }
383
384 fn build_zip(files: &[(&str, &[u8])]) -> Vec<u8> {
385 let mut buf = Vec::new();
386 let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
387 let options = zip::write::SimpleFileOptions::default();
388 for (name, content) in files {
389 zip.start_file(*name, options).unwrap();
390 zip.write_all(content).unwrap();
391 }
392 zip.finish().unwrap();
393 buf
394 }
395
396 fn fs_docs_storage(dir: &tempfile::TempDir) -> Arc<DocsStorage> {
397 Arc::new(DocsStorage::new(
398 Box::new(FSStorage::new(dir.path().to_str().unwrap()).unwrap()) as DynStorage,
399 ))
400 }
401
402 fn publish_request(uri: &str, token: &str, zip: Vec<u8>) -> Request<Body> {
403 Request::put(uri)
404 .header(header::AUTHORIZATION, token)
405 .body(Body::from(zip))
406 .unwrap()
407 }
408
409 #[tokio::test]
410 async fn publish_docs_stores_docs_under_lowercase_name_and_links_rustdoc_folder() {
411 let (state, db) = sqlite_test_state().await;
412 add_crate(&db, "My-Crate", "1.0.0").await;
413 let docs_storage = memory_docs_storage();
414 let zip = build_zip(&[("doc/My_Crate/index.html", b"<html>docs</html>")]);
415
416 let r = app(state, docs_storage.clone())
417 .oneshot(publish_request("/MY-crate/1.0.0", ADMIN_TOKEN, zip))
418 .await
419 .unwrap();
420
421 assert_eq!(r.status(), StatusCode::OK);
422 let key = DocsStorage::file_key(
423 &docs_name("my-crate"),
424 &Version::try_from("1.0.0").unwrap(),
425 "doc/My_Crate/index.html",
426 );
427 assert!(docs_storage.exists(&key).await.unwrap());
428 let body = r.into_body().collect().await.unwrap().to_bytes();
429 let response: DocUploadResponse = serde_json::from_slice(&body).unwrap();
430 assert_eq!(response.crate_name, "My-Crate");
431 assert_eq!(
432 docs_link(&db, "my-crate").await.as_deref(),
433 Some("/docs/my-crate/1.0.0/doc/My_Crate/index.html")
434 );
435 }
436
437 #[tokio::test]
440 async fn publish_docs_under_other_separator_stores_docs_under_published_name() {
441 let (state, db) = sqlite_test_state().await;
442 add_crate(&db, "My-Crate", "1.0.0").await;
443 let docs_storage = memory_docs_storage();
444 let zip = build_zip(&[("doc/My_Crate/index.html", b"<html>docs</html>")]);
445
446 let r = app(state, docs_storage.clone())
447 .oneshot(publish_request("/my_crate/1.0.0", ADMIN_TOKEN, zip))
448 .await
449 .unwrap();
450
451 assert_eq!(r.status(), StatusCode::OK);
452 let version = Version::try_from("1.0.0").unwrap();
453 let key =
454 DocsStorage::file_key(&docs_name("my-crate"), &version, "doc/My_Crate/index.html");
455 assert!(docs_storage.exists(&key).await.unwrap());
456 let other = DocsStorage::version_prefix(&docs_name("my_crate"), &version);
457 assert!(docs_storage.list(&other).await.unwrap().is_empty());
458 assert_eq!(
459 docs_link(&db, "my-crate").await.as_deref(),
460 Some("/docs/my-crate/1.0.0/doc/My_Crate/index.html")
461 );
462 }
463
464 fn docs_name(name: &str) -> DocsName {
465 DocsName::from(&OriginalName::try_from(name).unwrap())
466 }
467
468 #[tokio::test]
469 async fn publish_docs_uploads_files_and_updates_docs_link() {
470 let (state, db) = sqlite_test_state().await;
471 add_crate(&db, "test-crate", "1.0.0").await;
472 let tmp = tempfile::tempdir().unwrap();
473 let docs_storage = fs_docs_storage(&tmp);
474 let zip = build_zip(&[("doc/test_crate/index.html", b"<html>docs</html>")]);
475
476 let r = app(state, docs_storage.clone())
477 .oneshot(publish_request("/test-crate/1.0.0", ADMIN_TOKEN, zip))
478 .await
479 .unwrap();
480
481 assert_eq!(r.status(), StatusCode::OK);
482 let key = DocsStorage::file_key(
483 &docs_name("test-crate"),
484 &Version::try_from("1.0.0").unwrap(),
485 "doc/test_crate/index.html",
486 );
487 let stored = docs_storage.get(&key).await.unwrap();
488 assert_eq!(stored, bytes::Bytes::from_static(b"<html>docs</html>"));
489 assert_eq!(
490 docs_link(&db, "test-crate").await.as_deref(),
491 Some("/docs/test-crate/1.0.0/doc/test_crate/index.html")
492 );
493 }
494
495 #[tokio::test]
496 async fn publish_docs_rejects_read_only_owner() {
497 let (state, db) = sqlite_test_state().await;
498 let token = token_user(&db, "reader", UserFlags::read_only()).await;
499 TestCrateBuilder::new(&db)
500 .name("test-crate")
501 .owner("reader")
502 .build()
503 .await
504 .unwrap();
505 let tmp = tempfile::tempdir().unwrap();
506 let docs_storage = fs_docs_storage(&tmp);
507 let zip = build_zip(&[("doc/test_crate/index.html", b"<html>docs</html>")]);
508
509 let r = app(state, docs_storage.clone())
510 .oneshot(publish_request("/test-crate/1.0.0", &token, zip))
511 .await
512 .unwrap();
513
514 assert_eq!(r.status(), StatusCode::FORBIDDEN);
515 let key = DocsStorage::file_key(
516 &docs_name("test-crate"),
517 &Version::try_from("1.0.0").unwrap(),
518 "doc/test_crate/index.html",
519 );
520 assert!(!docs_storage.exists(&key).await.unwrap());
521 assert_eq!(docs_link(&db, "test-crate").await, None);
522 }
523
524 #[tokio::test]
527 async fn publish_docs_non_owner_cannot_tell_missing_from_existing_crate() {
528 let (state, db) = sqlite_test_state().await;
529 add_crate(&db, "test-crate", "1.0.0").await;
530 let token = token_user(&db, "other", UserFlags::normal()).await;
531 let tmp = tempfile::tempdir().unwrap();
532 let docs_storage = fs_docs_storage(&tmp);
533
534 for uri in ["/test-crate/1.0.0", "/test-crate/9.9.9", "/missing/1.0.0"] {
535 let zip = build_zip(&[("doc/x/index.html", b"<html>docs</html>")]);
536 let r = app(state.clone(), docs_storage.clone())
537 .oneshot(publish_request(uri, &token, zip))
538 .await
539 .unwrap();
540 assert_eq!(r.status(), StatusCode::FORBIDDEN, "{uri}");
541 }
542 assert_eq!(docs_link(&db, "test-crate").await, None);
543 }
544
545 #[tokio::test]
546 async fn publish_docs_propagates_extraction_failure() {
547 let (state, db) = sqlite_test_state().await;
552 add_crate(&db, "test-crate", "1.0.0").await;
553 let tmp = tempfile::tempdir().unwrap();
554 let zip = build_zip(&[("../escape.html", b"evil")]);
555
556 let r = app(state, fs_docs_storage(&tmp))
557 .oneshot(publish_request("/test-crate/1.0.0", ADMIN_TOKEN, zip))
558 .await
559 .unwrap();
560
561 assert_eq!(r.status(), StatusCode::BAD_REQUEST);
564 assert_eq!(docs_link(&db, "test-crate").await, None);
565 }
566
567 async fn docs_storage_with_latest_docs() -> (tempfile::TempDir, Arc<DocsStorage>) {
568 let tmp = tempfile::tempdir().unwrap();
569 let docs_storage = fs_docs_storage(&tmp);
570 let key = DocsStorage::file_key(
571 &docs_name("secret"),
572 &Version::try_from("1.0.0").unwrap(),
573 "doc/secret/index.html",
574 );
575 docs_storage
576 .put(&key, bytes::Bytes::from_static(b"<html>docs</html>"))
577 .await
578 .unwrap();
579 (tmp, docs_storage)
580 }
581
582 async fn state_with_crate(restricted: bool) -> (AppStateData, TestDb) {
584 let (state, db) = sqlite_test_state().await;
585 add_restricted_crate(&db, "secret", restricted).await;
586 (state, db)
587 }
588
589 async fn latest_location(
590 state: AppStateData,
591 docs_storage: Arc<DocsStorage>,
592 token: Option<&str>,
593 ) -> String {
594 let mut request = Request::get("/secret/latest");
595 if let Some(token) = token {
596 request = request.header(header::AUTHORIZATION, token);
597 }
598 let r = app(state, docs_storage)
599 .oneshot(request.body(Body::empty()).unwrap())
600 .await
601 .unwrap();
602 assert_eq!(r.status(), StatusCode::TEMPORARY_REDIRECT);
603 r.headers()[header::LOCATION].to_str().unwrap().to_string()
604 }
605
606 #[tokio::test]
607 async fn latest_docs_redirects_to_docs_for_unrestricted_crate() {
608 let (_tmp, docs_storage) = docs_storage_with_latest_docs().await;
609 let (state, _db) = state_with_crate(false).await;
610
611 let location = latest_location(state, docs_storage, None).await;
612
613 assert_eq!(location, "/docs/secret/1.0.0/doc/secret/index.html");
614 }
615
616 #[tokio::test]
617 async fn latest_docs_redirects_to_docs_of_crate_with_uppercase_letters() {
618 let docs_storage = memory_docs_storage();
619 let key = DocsStorage::file_key(
620 &docs_name("my-crate"),
621 &Version::try_from("1.0.0").unwrap(),
622 "doc/My_Crate/index.html",
623 );
624 docs_storage
625 .put(&key, bytes::Bytes::from_static(b"<html>docs</html>"))
626 .await
627 .unwrap();
628 let (state, db) = sqlite_test_state().await;
629 add_crate(&db, "My-Crate", "1.0.0").await;
630
631 let r = app(state, docs_storage)
632 .oneshot(
633 Request::get("/My-Crate/latest")
634 .body(Body::empty())
635 .unwrap(),
636 )
637 .await
638 .unwrap();
639
640 assert_eq!(
641 r.headers()[header::LOCATION],
642 "/docs/my-crate/1.0.0/doc/My_Crate/index.html"
643 );
644 }
645
646 #[tokio::test]
647 async fn latest_docs_hides_restricted_crate_from_anonymous_user() {
648 let (_tmp, docs_storage) = docs_storage_with_latest_docs().await;
649 let (state, _db) = state_with_crate(true).await;
650
651 let location = latest_location(state, docs_storage, None).await;
652
653 assert_eq!(location, "/");
654 }
655
656 #[tokio::test]
657 async fn latest_docs_hides_restricted_crate_from_user_without_access() {
658 let (_tmp, docs_storage) = docs_storage_with_latest_docs().await;
659 let (state, db) = state_with_crate(true).await;
660 let token = token_user(&db, "bob", UserFlags::normal()).await;
661
662 let location = latest_location(state, docs_storage, Some(&token)).await;
663
664 assert_eq!(location, "/");
665 }
666
667 #[tokio::test]
668 async fn latest_docs_redirects_crate_user_of_restricted_crate() {
669 let (_tmp, docs_storage) = docs_storage_with_latest_docs().await;
670 let (state, db) = state_with_crate(true).await;
671 let token = token_user(&db, "alice", UserFlags::normal()).await;
672 db.add_crate_user(&canonical("secret"), &user_name("alice"))
673 .await
674 .unwrap();
675
676 let location = latest_location(state, docs_storage, Some(&token)).await;
677
678 assert_eq!(location, "/docs/secret/1.0.0/doc/secret/index.html");
679 }
680
681 #[tokio::test]
682 async fn latest_docs_redirects_owner_of_restricted_crate() {
683 let (_tmp, docs_storage) = docs_storage_with_latest_docs().await;
684 let (state, db) = state_with_crate(true).await;
685 let token = token_user(&db, "olivia", UserFlags::normal()).await;
686 db.add_owner(&canonical("secret"), &user_name("olivia"))
687 .await
688 .unwrap();
689
690 let location = latest_location(state, docs_storage, Some(&token)).await;
691
692 assert_eq!(location, "/docs/secret/1.0.0/doc/secret/index.html");
693 }
694
695 #[tokio::test]
696 async fn latest_docs_redirects_group_member_of_restricted_crate() {
697 let (_tmp, docs_storage) = docs_storage_with_latest_docs().await;
698 let (state, db) = state_with_crate(true).await;
699 let token = token_user(&db, "greg", UserFlags::normal()).await;
700 let group = GroupName::new("readers").unwrap();
701 db.add_group(&group).await.unwrap();
702 db.add_group_user(&group, &user_name("greg")).await.unwrap();
703 db.add_crate_group(&canonical("secret"), &group)
704 .await
705 .unwrap();
706
707 let location = latest_location(state, docs_storage, Some(&token)).await;
708
709 assert_eq!(location, "/docs/secret/1.0.0/doc/secret/index.html");
710 }
711
712 #[tokio::test]
713 async fn latest_docs_hides_crate_when_restriction_check_fails() {
714 let (_tmp, docs_storage) = docs_storage_with_latest_docs().await;
715 let (state, db) = state_with_crate(false).await;
716 test_drop_table(&db, "krate").await.unwrap();
717
718 let location = latest_location(state, docs_storage, None).await;
719
720 assert_eq!(location, "/");
721 }
722
723 #[tokio::test]
724 async fn latest_docs_hides_restricted_crate_when_access_check_fails() {
725 let (_tmp, docs_storage) = docs_storage_with_latest_docs().await;
726 let (state, db) = state_with_crate(true).await;
727 let token = token_user(&db, "olivia", UserFlags::normal()).await;
729 db.add_owner(&canonical("secret"), &user_name("olivia"))
730 .await
731 .unwrap();
732 test_drop_table(&db, "crate_user").await.unwrap();
733
734 let location = latest_location(state, docs_storage, Some(&token)).await;
735
736 assert_eq!(location, "/");
737 }
738}