crates/cli/git-ents/tests/serve.rs
serve.rshistorycomment on this file
| 1 | //! Integration coverage for `git ents serve`'s wiring (`roots.local`): |
| 2 | //! [`git_ents::commands::serve::build_state`] reuses a real |
| 3 | //! [`LocalRoot`]'s own seams (the same loose-ref `RefStore` and odb every |
| 4 | //! other porcelain command uses), signs with the local user's own key |
| 5 | //! (`roots.web-signing`), and the resulting `ents-web` router carries no |
| 6 | //! git smart-HTTP surface — all driven in-process via |
| 7 | //! `tower::ServiceExt::oneshot`, no socket ever bound (`roots.web-agnostic`). |
| 8 | #![allow(clippy::expect_used, reason = "integration test")] |
| 9 | |
| 10 | mod common; |
| 11 | |
| 12 | use axum::body::Body; |
| 13 | use axum::http::{Request, StatusCode}; |
| 14 | use git_ents::commands::members; |
| 15 | use git_ents::root::LocalRoot; |
| 16 | use tower::ServiceExt as _; |
| 17 | |
| 18 | /// `roots.local`: `git ents serve`'s state is built from an already-open |
| 19 | /// [`LocalRoot`] (never a second store), and the router it drives exposes |
| 20 | /// only the web UI — no `/info/refs`, no git wire protocol. |
| 21 | #[tokio::test] |
| 22 | // @relation(roots.local, roots.composition, scope=function, role=Verifies) |
| 23 | async fn serve_reuses_the_local_root_and_exposes_no_git_transport() { |
| 24 | let fixture = common::Fixture::new(1); |
| 25 | let root = LocalRoot::open(fixture.path()).expect("opens"); |
| 26 | members::add(&root, "jdc", None, Some(fixture.key_path.clone())).expect("bootstrap"); |
| 27 | |
| 28 | let root = LocalRoot::open(fixture.path()).expect("reopen for serve"); |
| 29 | let state = git_ents::commands::serve::build_state(root, Some(fixture.key_path.clone())) |
| 30 | .expect("builds state from the local root"); |
| 31 | let router = ents_web::router(state); |
| 32 | |
| 33 | let dashboard = router |
| 34 | .clone() |
| 35 | .oneshot(Request::get("/").body(Body::empty()).expect("request")) |
| 36 | .await |
| 37 | .expect("in-process call"); |
| 38 | assert_eq!(dashboard.status(), StatusCode::OK); |
| 39 | |
| 40 | let members_page = router |
| 41 | .clone() |
| 42 | .oneshot( |
| 43 | Request::get("/members") |
| 44 | .body(Body::empty()) |
| 45 | .expect("request"), |
| 46 | ) |
| 47 | .await |
| 48 | .expect("in-process call"); |
| 49 | assert_eq!(members_page.status(), StatusCode::OK); |
| 50 | |
| 51 | let smart_http = router |
| 52 | .oneshot( |
| 53 | Request::get("/info/refs?service=git-upload-pack") |
| 54 | .body(Body::empty()) |
| 55 | .expect("request"), |
| 56 | ) |
| 57 | .await |
| 58 | .expect("in-process call"); |
| 59 | assert_eq!( |
| 60 | smart_http.status(), |
| 61 | StatusCode::NOT_FOUND, |
| 62 | "git ents serve must never expose git's own smart-HTTP transport" |
| 63 | ); |
| 64 | } |
| 65 | |
| 66 | /// `roots.web-signing`: the identity chip shows the signer's own enrolled |
| 67 | /// member username, resolved via `commands::members::find_by_key` (the |
| 68 | /// same key-match loop `git ents members check` runs) -- not |
| 69 | /// `actor().name`, which is a fixed `"git-ents"` commit-author wordmark |
| 70 | /// that would otherwise just duplicate the site logo next to it. |
| 71 | #[tokio::test] |
| 72 | // @relation(roots.web-signing, scope=function, role=Verifies) |
| 73 | async fn serve_identity_chip_shows_the_signers_enrolled_member_username() { |
| 74 | let fixture = common::Fixture::new(1); |
| 75 | let root = LocalRoot::open(fixture.path()).expect("opens"); |
| 76 | members::add(&root, "jdc", None, Some(fixture.key_path.clone())).expect("bootstrap"); |
| 77 | |
| 78 | let root = LocalRoot::open(fixture.path()).expect("reopen for serve"); |
| 79 | let state = git_ents::commands::serve::build_state(root, Some(fixture.key_path.clone())) |
| 80 | .expect("builds state from the local root"); |
| 81 | let router = ents_web::router(state); |
| 82 | |
| 83 | let response = router |
| 84 | .oneshot(Request::get("/").body(Body::empty()).expect("request")) |
| 85 | .await |
| 86 | .expect("in-process call"); |
| 87 | assert_eq!(response.status(), StatusCode::OK); |
| 88 | let body = String::from_utf8( |
| 89 | axum::body::to_bytes(response.into_body(), usize::MAX) |
| 90 | .await |
| 91 | .expect("body") |
| 92 | .to_vec(), |
| 93 | ) |
| 94 | .expect("utf8 html"); |
| 95 | assert!( |
| 96 | body.contains(r#"class="id-chip" href="/account""#) && body.contains("<span>jdc</span>"), |
| 97 | "the id-chip must show the enrolled member's own username: {body}" |
| 98 | ); |
| 99 | } |
| 100 | |
| 101 | /// A signer whose key names no enrolled member falls back to a short key |
| 102 | /// fingerprint for the identity chip -- never `actor()`'s `"git-ents"` |
| 103 | /// wordmark, which would silently duplicate the site logo. |
| 104 | #[tokio::test] |
| 105 | // @relation(roots.web-signing, scope=function, role=Verifies) |
| 106 | async fn serve_identity_chip_falls_back_to_a_fingerprint_when_unenrolled() { |
| 107 | let fixture = common::Fixture::new(2); |
| 108 | let root = LocalRoot::open(fixture.path()).expect("opens"); |
| 109 | let state = git_ents::commands::serve::build_state(root, Some(fixture.key_path.clone())) |
| 110 | .expect("builds state from the local root"); |
| 111 | let router = ents_web::router(state); |
| 112 | |
| 113 | let response = router |
| 114 | .oneshot(Request::get("/").body(Body::empty()).expect("request")) |
| 115 | .await |
| 116 | .expect("in-process call"); |
| 117 | assert_eq!(response.status(), StatusCode::OK); |
| 118 | let body = String::from_utf8( |
| 119 | axum::body::to_bytes(response.into_body(), usize::MAX) |
| 120 | .await |
| 121 | .expect("body") |
| 122 | .to_vec(), |
| 123 | ) |
| 124 | .expect("utf8 html"); |
| 125 | let chip = body |
| 126 | .split(r#"class="id-chip" href="/account">"#) |
| 127 | .nth(1) |
| 128 | .and_then(|rest| rest.split("</a>").next()) |
| 129 | .expect("id-chip renders"); |
| 130 | assert_ne!( |
| 131 | chip, "git-ents", |
| 132 | "an unenrolled key must never fall back to the commit-author wordmark" |
| 133 | ); |
| 134 | assert!(!chip.is_empty(), "the fingerprint fallback is never blank"); |
| 135 | } |