From 77cb1588dc42ce90fd93d27201207dbf5a485342 Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Tue, 17 Dec 2024 10:33:38 +0100 Subject: [PATCH 01/13] refactor: Joined git and serve method --- src/server/api/routes.rs | 58 +++++++++++++++++++- src/server/api/state.rs | 8 +++ src/server/app.rs | 4 +- src/server/git.rs | 112 --------------------------------------- src/server/mod.rs | 1 - src/utils/cli.rs | 8 --- 6 files changed, 67 insertions(+), 124 deletions(-) delete mode 100644 src/server/git.rs diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs index 41ea8c9..5555350 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -11,10 +11,18 @@ use actix_service::ServiceFactory; use actix_web::{ body::MessageBody, dev::{ServiceRequest, ServiceResponse}, - guard, web, App, Error, Scope, + guard, web, App, Error, Scope, Responder, route, + HttpResponse }; +use crate::utils::http::get_contenttype; +use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; use super::{serve::serve, state::Global, versions::versions}; +use crate::utils::paths::clean_path; +use git2::{self, ErrorCode}; +use super::state::App as AppState; + +use super::super::errors::HTTPError; /// Name of the header to guard current documents static HEADER_NAME: OnceLock<String> = OnceLock::new(); @@ -315,3 +323,51 @@ fn register_dependent_routes( } Ok(()) } + + +/// Return the content in the stelae archive in the `{namespace}/{name}` +/// repo at the `commitish` commit at the `remainder` path. +/// Return 404 if any are not found or there are any errors. +#[route( + "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}", + method = "GET", + method = "HEAD" +)] +#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))] +async fn get_blob( + path: web::Path<(String, String, String, String)>, + data: web::Data<AppState>, +) -> impl Responder { + let (namespace, name, commitish, remainder) = path.into_inner(); + let archive_path = &data.archive_path; + let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); + let blob_path = clean_path(&remainder); + let contenttype = get_contenttype(&blob_path); + match blob { + Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content), + Err(error) => blob_error_response(&error, &namespace, &name), + } +} + +/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses +#[allow(clippy::wildcard_enum_match_arm)] +#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))] +fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse { + tracing::error!("{error}",); + if let Some(git_error) = error.downcast_ref::<git2::Error>() { + return match git_error.code() { + // TODO: check this is the right error + ErrorCode::NotFound => { + HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist")) + } + _ => HttpResponse::InternalServerError().body("Unexpected Git error"), + }; + } + match error { + // TODO: Obviously it's better to use custom `Error` types + _ if error.to_string() == GIT_REQUEST_NOT_FOUND => { + HttpResponse::NotFound().body(HTTPError::NotFound.to_string()) + } + _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()), + } +} \ No newline at end of file diff --git a/src/server/api/state.rs b/src/server/api/state.rs index 172be04..b78c7db 100644 --- a/src/server/api/state.rs +++ b/src/server/api/state.rs @@ -13,6 +13,8 @@ pub trait Global { fn archive(&self) -> &Archive; /// Database connection fn db(&self) -> &db::DatabaseConnection; + /// path to the Stelae archive + fn archive_path(&self) -> &PathBuf; } /// Application state @@ -22,6 +24,8 @@ pub struct App { pub archive: Archive, /// Database connection pub db: db::DatabaseConnection, + /// path to the Stelae archive + pub archive_path: PathBuf, } impl Global for App { @@ -32,6 +36,10 @@ impl Global for App { fn db(&self) -> &db::DatabaseConnection { &self.db } + + fn archive_path(&self) -> &PathBuf { + &self.archive_path + } } /// Repository to serve diff --git a/src/server/app.rs b/src/server/app.rs index 0acf600..246e747 100644 --- a/src/server/app.rs +++ b/src/server/app.rs @@ -44,7 +44,7 @@ pub async fn serve_archive( } }; - let archive = match Archive::parse(archive_path, &PathBuf::from(raw_archive_path), individual) { + let archive = match Archive::parse(archive_path.clone(), &PathBuf::from(raw_archive_path), individual) { Ok(archive) => archive, Err(err) => { tracing::error!("Unable to parse archive at '{raw_archive_path}'."); @@ -53,7 +53,7 @@ pub async fn serve_archive( } }; - let state = AppState { archive, db }; + let state = AppState { archive, db, archive_path }; HttpServer::new(move || { init(&state).unwrap_or_else(|err| { diff --git a/src/server/git.rs b/src/server/git.rs deleted file mode 100644 index 992f835..0000000 --- a/src/server/git.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Legacy git microserver. - -use actix_web::{get, route, web, App, HttpResponse, HttpServer, Responder}; -use git2::{self, ErrorCode}; -use std::path::PathBuf; -use tracing_actix_web::TracingLogger; - -use super::errors::{CliError, HTTPError, StelaeError}; -use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; -use crate::utils::http::get_contenttype; -use crate::{server::tracing::StelaeRootSpanBuilder, utils::paths::clean_path}; - -/// Global, read-only state passed into the actix app -struct AppState { - /// path to the Stelae archive - archive_path: PathBuf, -} - -/// Root index path -#[get("/")] -async fn index() -> &'static str { - "Welcome to Stelae" -} - -/// Just for development purposes at the moment -#[get("{path}")] -async fn misc(path: web::Path<String>) -> actix_web::Result<&'static str, StelaeError> { - match path.as_str() { - "error" => Err(StelaeError::GitError), - _ => Ok("\u{2728}"), - } -} - -/// Return the content in the stelae archive in the `{namespace}/{name}` -/// repo at the `commitish` commit at the `remainder` path. -/// Return 404 if any are not found or there are any errors. -#[route( - "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}", - method = "GET", - method = "HEAD" -)] -#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))] -async fn get_blob( - path: web::Path<(String, String, String, String)>, - data: web::Data<AppState>, -) -> impl Responder { - let (namespace, name, commitish, remainder) = path.into_inner(); - let archive_path = &data.archive_path; - let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); - let blob_path = clean_path(&remainder); - let contenttype = get_contenttype(&blob_path); - match blob { - Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content), - Err(error) => blob_error_response(&error, &namespace, &name), - } -} - -/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses -#[expect( - clippy::wildcard_enum_match_arm, - reason = "Default to wildcard match in case of unexpected errors" -)] -#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))] -fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse { - tracing::error!("{error}",); - if let Some(git_error) = error.downcast_ref::<git2::Error>() { - return match git_error.code() { - // TODO: check this is the right error - ErrorCode::NotFound => { - HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist")) - } - _ => HttpResponse::InternalServerError().body("Unexpected Git error"), - }; - } - match error { - // TODO: Obviously it's better to use custom `Error` types - _ if error.to_string() == GIT_REQUEST_NOT_FOUND => { - HttpResponse::NotFound().body(HTTPError::NotFound.to_string()) - } - _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()), - } -} - -/// Serve git repositories in the Stelae archive. -#[actix_web::main] // or #[tokio::main] -pub async fn serve_git( - raw_archive_path: &str, - archive_path: PathBuf, - port: u16, -) -> Result<(), CliError> { - let bind = "127.0.0.1"; - let message = "Serving content from the Stelae archive at"; - tracing::info!("{message} '{raw_archive_path}' on http://{bind}:{port}.",); - - HttpServer::new(move || { - App::new() - .wrap(TracingLogger::<StelaeRootSpanBuilder>::new()) - .service(index) - .service(misc) - .service(get_blob) - .app_data(web::Data::new(AppState { - archive_path: archive_path.clone(), - })) - }) - .bind((bind, port))? - .run() - .await - .map_err(|err| { - tracing::error!("Error running Git server: {err:?}"); - CliError::GenericError - }) -} diff --git a/src/server/mod.rs b/src/server/mod.rs index c287916..286c7ec 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -5,5 +5,4 @@ pub mod api; pub mod app; pub mod errors; -pub mod git; pub mod tracing; diff --git a/src/utils/cli.rs b/src/utils/cli.rs index 1333d5f..ffd4c80 100644 --- a/src/utils/cli.rs +++ b/src/utils/cli.rs @@ -7,7 +7,6 @@ use crate::history::changes; use crate::server::app::serve_archive; use crate::server::errors::CliError; -use crate::server::git::serve_git; use crate::utils::archive::find_archive_path; use clap::Parser; use std::env; @@ -41,12 +40,6 @@ struct Cli { /// Subcommands for the Stelae CLI #[derive(Clone, clap::Subcommand)] enum Subcommands { - /// Serve git repositories in the Stelae archive - Git { - /// Port on which to serve the archive. - #[arg(short, long, default_value_t = 8080)] - port: u16, - }, /// Serve documents in a Stelae archive. Serve { /// Port on which to serve the archive. @@ -115,7 +108,6 @@ fn init_tracing(archive_path: &Path) { /// This function returns the generic `CliError`, based on which we exit with a known exit code. fn execute_command(cli: &Cli, archive_path: PathBuf) -> Result<(), CliError> { match cli.subcommands { - Subcommands::Git { port } => serve_git(&cli.archive_path, archive_path, port), Subcommands::Serve { port, individual } => { serve_archive(&cli.archive_path, archive_path, port, individual) } From 73925fdec600388a1e6f6cd3f147a168c1957e2b Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Mon, 9 Dec 2024 09:40:20 +0100 Subject: [PATCH 02/13] REFACTOR: Merged git and serve command --- src/server/api/routes.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs index 5555350..e8a4378 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -11,8 +11,13 @@ use actix_service::ServiceFactory; use actix_web::{ body::MessageBody, dev::{ServiceRequest, ServiceResponse}, +<<<<<<< HEAD guard, web, App, Error, Scope, Responder, route, HttpResponse +======= + guard, web, App, Error, Scope, get, Responder, + HttpResponse, HttpRequest +>>>>>>> ac49acd (Merging git and serve) }; use crate::utils::http::get_contenttype; @@ -77,6 +82,13 @@ pub fn register_app< .service(web::resource("/{path:.*}").to(versions)) .service(web::resource("").to(versions)), ), + + ) + .app_data(web::Data::new(state.clone())); + + app = app + .service( + get_blob ) .app_data(web::Data::new(state.clone())); @@ -328,17 +340,23 @@ fn register_dependent_routes( /// Return the content in the stelae archive in the `{namespace}/{name}` /// repo at the `commitish` commit at the `remainder` path. /// Return 404 if any are not found or there are any errors. +<<<<<<< HEAD #[route( "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}", method = "GET", method = "HEAD" )] +======= +#[get("/{namespace}/{name}/ref_{commitish:.*}_/{remainder}")]//:/+([^{}]*?)?/*}")] +>>>>>>> ac49acd (Merging git and serve) #[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))] async fn get_blob( path: web::Path<(String, String, String, String)>, data: web::Data<AppState>, ) -> impl Responder { + println!("TEST"); let (namespace, name, commitish, remainder) = path.into_inner(); + println!("{commitish}"); let archive_path = &data.archive_path; let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); let blob_path = clean_path(&remainder); From 651bbb44b4e3ab120521fab667e3027fe2cdbcef Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Wed, 11 Dec 2024 09:38:51 +0100 Subject: [PATCH 03/13] REFACTOR: fixed merge conflicts --- src/server/api/routes.rs | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs index e8a4378..d4d13cb 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -16,9 +16,14 @@ use actix_web::{ HttpResponse ======= guard, web, App, Error, Scope, get, Responder, +<<<<<<< HEAD HttpResponse, HttpRequest >>>>>>> ac49acd (Merging git and serve) +======= + HttpResponse +>>>>>>> 6268aa5 (Updated path for stelae git and removed redundant code) }; +use serde::Deserialize; use crate::utils::http::get_contenttype; use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; @@ -88,7 +93,9 @@ pub fn register_app< app = app .service( - get_blob + web::scope("/_git").service( + get_blob + ) ) .app_data(web::Data::new(state.clone())); @@ -336,11 +343,20 @@ fn register_dependent_routes( Ok(()) } +/// Structure for +#[derive(Debug, Deserialize)] +struct Info { + /// commit of the repo + commitish: String, + /// path of the file + remainder: Option<String>, +} /// Return the content in the stelae archive in the `{namespace}/{name}` /// repo at the `commitish` commit at the `remainder` path. /// Return 404 if any are not found or there are any errors. <<<<<<< HEAD +<<<<<<< HEAD #[route( "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}", method = "GET", @@ -350,13 +366,19 @@ fn register_dependent_routes( #[get("/{namespace}/{name}/ref_{commitish:.*}_/{remainder}")]//:/+([^{}]*?)?/*}")] >>>>>>> ac49acd (Merging git and serve) #[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))] +======= +#[get("/{namespace}/{name}")]//:/+([^{}]*?)?/*}")] +#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, info))] +>>>>>>> 6268aa5 (Updated path for stelae git and removed redundant code) async fn get_blob( - path: web::Path<(String, String, String, String)>, - data: web::Data<AppState>, + path: web::Path<(String, String)>, + info: web::Query<Info>, + data: web::Data<AppState> ) -> impl Responder { - println!("TEST"); - let (namespace, name, commitish, remainder) = path.into_inner(); - println!("{commitish}"); + let (namespace, name/* , commitish, remainder*/) = path.into_inner(); + let info_struct: Info = info.into_inner(); + let commitish = info_struct.commitish; + let remainder = info_struct.remainder.unwrap_or_else(|| "".to_string()); let archive_path = &data.archive_path; let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); let blob_path = clean_path(&remainder); From 98b132f649342b2ce061b68377fa9fc82fb6610d Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Tue, 17 Dec 2024 11:30:22 +0100 Subject: [PATCH 04/13] REFACTOR: executed cargo fmt and removed stale merge confict tags --- src/server/api/routes.rs | 48 ++++++++++++---------------------------- src/server/app.rs | 12 ++++++++-- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs index d4d13cb..a48bf55 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -11,26 +11,16 @@ use actix_service::ServiceFactory; use actix_web::{ body::MessageBody, dev::{ServiceRequest, ServiceResponse}, -<<<<<<< HEAD - guard, web, App, Error, Scope, Responder, route, - HttpResponse -======= - guard, web, App, Error, Scope, get, Responder, -<<<<<<< HEAD - HttpResponse, HttpRequest ->>>>>>> ac49acd (Merging git and serve) -======= - HttpResponse ->>>>>>> 6268aa5 (Updated path for stelae git and removed redundant code) + guard, route, web, App, Error, HttpResponse, Responder, Scope, }; use serde::Deserialize; -use crate::utils::http::get_contenttype; -use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; +use super::state::App as AppState; use super::{serve::serve, state::Global, versions::versions}; +use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; +use crate::utils::http::get_contenttype; use crate::utils::paths::clean_path; use git2::{self, ErrorCode}; -use super::state::App as AppState; use super::super::errors::HTTPError; @@ -87,16 +77,11 @@ pub fn register_app< .service(web::resource("/{path:.*}").to(versions)) .service(web::resource("").to(versions)), ), - ) .app_data(web::Data::new(state.clone())); - app = app - .service( - web::scope("/_git").service( - get_blob - ) - ) + app = app + .service(web::scope("/_git").service(get_blob)) .app_data(web::Data::new(state.clone())); app = register_dynamic_routes(app, state)?; @@ -343,7 +328,7 @@ fn register_dependent_routes( Ok(()) } -/// Structure for +/// Structure for #[derive(Debug, Deserialize)] struct Info { /// commit of the repo @@ -355,27 +340,22 @@ struct Info { /// Return the content in the stelae archive in the `{namespace}/{name}` /// repo at the `commitish` commit at the `remainder` path. /// Return 404 if any are not found or there are any errors. -<<<<<<< HEAD -<<<<<<< HEAD #[route( "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}", method = "GET", method = "HEAD" )] -======= -#[get("/{namespace}/{name}/ref_{commitish:.*}_/{remainder}")]//:/+([^{}]*?)?/*}")] ->>>>>>> ac49acd (Merging git and serve) -#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))] -======= -#[get("/{namespace}/{name}")]//:/+([^{}]*?)?/*}")] #[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, info))] ->>>>>>> 6268aa5 (Updated path for stelae git and removed redundant code) +#[expect( + clippy::future_not_send, + reason = "We don't worry about git2-rs not implementing `Send` trait" +)] async fn get_blob( path: web::Path<(String, String)>, info: web::Query<Info>, - data: web::Data<AppState> + data: web::Data<AppState>, ) -> impl Responder { - let (namespace, name/* , commitish, remainder*/) = path.into_inner(); + let (namespace, name /* , commitish, remainder*/) = path.into_inner(); let info_struct: Info = info.into_inner(); let commitish = info_struct.commitish; let remainder = info_struct.remainder.unwrap_or_else(|| "".to_string()); @@ -410,4 +390,4 @@ fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> Ht } _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()), } -} \ No newline at end of file +} diff --git a/src/server/app.rs b/src/server/app.rs index 246e747..7e645a6 100644 --- a/src/server/app.rs +++ b/src/server/app.rs @@ -44,7 +44,11 @@ pub async fn serve_archive( } }; - let archive = match Archive::parse(archive_path.clone(), &PathBuf::from(raw_archive_path), individual) { + let archive = match Archive::parse( + archive_path.clone(), + &PathBuf::from(raw_archive_path), + individual, + ) { Ok(archive) => archive, Err(err) => { tracing::error!("Unable to parse archive at '{raw_archive_path}'."); @@ -53,7 +57,11 @@ pub async fn serve_archive( } }; - let state = AppState { archive, db, archive_path }; + let state = AppState { + archive, + db, + archive_path, + }; HttpServer::new(move || { init(&state).unwrap_or_else(|err| { From 71ccad27c59d567a5d0aa40b1308153ef3ea5461 Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Tue, 17 Dec 2024 11:48:31 +0100 Subject: [PATCH 05/13] FIX: Git command supports commit names that contains / --- src/server/api/routes.rs | 12 ++++-------- tests/common/mod.rs | 3 +++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs index a48bf55..5a37704 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -340,11 +340,7 @@ struct Info { /// Return the content in the stelae archive in the `{namespace}/{name}` /// repo at the `commitish` commit at the `remainder` path. /// Return 404 if any are not found or there are any errors. -#[route( - "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}", - method = "GET", - method = "HEAD" -)] +#[route("/{namespace}/{name}", method = "GET", method = "HEAD")] #[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, info))] #[expect( clippy::future_not_send, @@ -355,10 +351,10 @@ async fn get_blob( info: web::Query<Info>, data: web::Data<AppState>, ) -> impl Responder { - let (namespace, name /* , commitish, remainder*/) = path.into_inner(); + let (namespace, name) = path.into_inner(); let info_struct: Info = info.into_inner(); let commitish = info_struct.commitish; - let remainder = info_struct.remainder.unwrap_or_else(|| "".to_string()); + let remainder = info_struct.remainder.unwrap_or_default(); let archive_path = &data.archive_path; let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); let blob_path = clean_path(&remainder); @@ -370,7 +366,7 @@ async fn get_blob( } /// A centralised place to match potentially unsafe internal errors to safe user-facing error responses -#[allow(clippy::wildcard_enum_match_arm)] +#[expect(clippy::wildcard_enum_match_arm, reason = "Allows _ for enum matching")] #[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))] fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse { tracing::error!("{error}",); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 34b113b..a09bd71 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -44,6 +44,9 @@ impl Global for TestAppState { fn db(&self) -> &db::DatabaseConnection { unimplemented!() } + fn archive_path(&self) -> &PathBuf { + unimplemented!() + } } pub async fn initialize_app( From 9f2660bc351564fc04ef7917ec2ba34419dc3c9c Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Wed, 1 Jan 2025 22:11:35 +0100 Subject: [PATCH 06/13] test: added test for get_blob api --- tests/api/archive_basic_test.rs | 306 +++++++++++++++++++++++++++++- tests/archive_testtools/config.rs | 2 + tests/archive_testtools/mod.rs | 23 +++ tests/common/mod.rs | 7 +- 4 files changed, 331 insertions(+), 7 deletions(-) diff --git a/tests/api/archive_basic_test.rs b/tests/api/archive_basic_test.rs index 4c2fd4f..3955d92 100644 --- a/tests/api/archive_basic_test.rs +++ b/tests/api/archive_basic_test.rs @@ -1,6 +1,11 @@ use crate::archive_testtools::config::{ArchiveType, Jurisdiction}; -use crate::common; -use actix_web::test; +use crate::archive_testtools::get_repository; +use crate::common::{self}; +use actix_http::Request; +use actix_service::Service; +use actix_web::body::MessageBody; +use actix_web::dev::ServiceResponse; +use actix_web::{test, Error}; #[actix_web::test] async fn test_resolve_law_html_request_with_full_path_expect_success() { @@ -189,3 +194,300 @@ async fn get_law_pdf_request_with_incorrect_path_expect_not_found() { let expected = true; assert_eq!(actual, expected); } + +#[actix_web::test] +async fn test_resolve_root_stele_all_repositories_request_with_full_path_expect_success() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + let mut path = archive_path.path().to_path_buf(); + path.push("test_org"); + + // let git_repo = get_repository(&path, "law-html"); + // let _ = git_repo.create_branch("test_branch"); + println!("Testing law-html"); + test_paths( + "law-html", + vec!["", "a/", "a/b/", "a/d/", "a/b/c.html", "a/b/c/"], + "HEAD", + &app, + true, + ) + .await; + + println!("Testing law-other"); + test_paths( + "law-other", + vec![ + "", + "example.json", + "a/", + "a/e/_doc/f/", + "a/d/", + "a/b/", + "a/b/c.html", + "a/_doc/e/", + "_prefix/", + "_prefix/a/", + ], + "HEAD", + &app, + true, + ) + .await; + + println!("Testing law-pdf"); + test_paths( + "law-pdf", + vec!["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"], + "HEAD", + &app, + true, + ) + .await; + + println!("Testing law-rdf"); + test_paths( + "law-rdf", + vec![ + "index.rdf", + "a/index.rdf", + "a/b/index.rdf", + "a/d/index.rdf", + "a/b/c.rdf", + "a/b/c/index.rdf", + ], + "HEAD", + &app, + true, + ) + .await; + + println!("Testing law-xml"); + test_paths( + "law-xml", + vec![ + "index.xml", + "a/index.xml", + "a/b/index.xml", + "a/d/index.xml", + "a/b/c.xml", + "a/b/c/index.xml", + ], + "HEAD", + &app, + true, + ) + .await; + + println!("Testing law-xml-codified"); + test_paths( + "law-xml-codified", + vec!["index.xml", "e/index.xml", "e/f/index.xml", "e/g/index.xml"], + "master", + &app, + true, + ) + .await; +} + +#[actix_web::test] +async fn test_resolve_root_stele_all_repositories_request_with_incorrect_path_expect_client_error() +{ + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + let mut path = archive_path.path().to_path_buf(); + path.push("test_org"); + + println!("Testing law-html"); + test_paths( + "law-html", + vec!["a/b/c/d", "a/index.css"], + "HEAD", + &app, + false, + ) + .await; + + println!("Testing law-other"); + test_paths( + "law-other", + vec!["a/b/c/d", "example1.json"], + "HEAD", + &app, + false, + ) + .await; + + println!("Testing law-pdf"); + test_paths( + "law-pdf", + vec!["/example1.pdf", "/c/example.pdf"], + "HEAD", + &app, + false, + ) + .await; + + println!("Testing law-rdf"); + test_paths( + "law-rdf", + vec!["index1.rdf", "z/index.rdf"], + "HEAD", + &app, + false, + ) + .await; + + println!("Testing law-xml"); + test_paths( + "law-xml", + vec!["index1.xml", "t/index.xml"], + "HEAD", + &app, + false, + ) + .await; + + println!("Testing law-xml-codified"); + test_paths( + "law-xml-codified", + vec!["index1.xml", "x/index.xml"], + "HEAD", + &app, + false, + ) + .await; +} + +async fn test_paths( + repo_name: &str, + file_paths: Vec<&str>, + branch_name: &str, + app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>, + expected: bool, +) { + for request_uri in file_paths { + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/{}?commitish={}&remainder={}", + repo_name, branch_name, request_uri + )) + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = if expected { + resp.status().is_success() + } else { + resp.status().is_client_error() + }; + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn test_resolve_root_stele_law_html_different_files_with_different_branches() { + let archive_path = + common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + let mut path = archive_path.path().to_path_buf(); + path.push("test_org"); + let git_repo = get_repository(&path, "law-html"); + path.push("law-html"); + let _ = git_repo.create_branch("test_branch"); + + let _ = git_repo.checkout("master"); + let _ = git_repo.add_file(&path, "test.txt", "Content for master branch"); + let _ = git_repo.commit(None, "Adding data for master branch"); + + let _ = git_repo.checkout("test_branch"); + let _ = git_repo.add_file(&path, "test1.txt", "Content for test branch"); + let _ = git_repo.commit(None, "Adding data for master branch"); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish=master&remainder=/test.txt" + )) + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + assert_eq!(actual, true); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish=master&remainder=/test1.txt" + )) + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + assert_eq!(actual, false); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish=test_branch&remainder=/test.txt" + )) + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + assert_eq!(actual, false); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish=test_branch&remainder=/test1.txt" + )) + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + assert_eq!(actual, true); +} + +#[actix_web::test] +async fn test_resolve_root_stele_law_html_file_content_with_different_branches_expect_success() { + let archive_path = + common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + let mut path = archive_path.path().to_path_buf(); + path.push("test_org"); + let git_repo = get_repository(&path, "law-html"); + path.push("law-html"); + + let _ = git_repo.create_branch("test_branch"); + + let _ = git_repo.checkout("master"); + let _ = git_repo.add_file(&path, "test.txt", "Content for master branch"); + let _ = git_repo.commit(None, "Adding data for master branch"); + + let _ = git_repo.checkout("test_branch"); + let _ = git_repo.add_file(&path, "test.txt", "Content for test branch"); + let _ = git_repo.commit(None, "Adding data for test branch"); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish=master&remainder=/test.txt" + )) + .to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = "Content for master branch"; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish=test_branch&remainder=test.txt" + )) + .to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = "Content for test branch"; + println!("{}", common::blob_to_string(actual.to_vec())); + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); +} diff --git a/tests/archive_testtools/config.rs b/tests/archive_testtools/config.rs index 991aa30..dca7331 100644 --- a/tests/archive_testtools/config.rs +++ b/tests/archive_testtools/config.rs @@ -12,6 +12,7 @@ pub enum Jurisdiction { Multi, } +#[derive(Debug)] pub enum TestDataRepositoryType { Html, Rdf, @@ -23,6 +24,7 @@ pub enum TestDataRepositoryType { /// Information about a data repository. /// /// This struct is used to initialize a data repository in the test suite. +#[derive(Debug)] pub struct TestDataRepositoryContext { /// The name of the data repository. pub name: String, diff --git a/tests/archive_testtools/mod.rs b/tests/archive_testtools/mod.rs index 0d08cd5..cbbee03 100644 --- a/tests/archive_testtools/mod.rs +++ b/tests/archive_testtools/mod.rs @@ -93,6 +93,29 @@ impl GitRepository { path: path.to_path_buf(), }) } + + pub fn create_branch(&self, branch_name: &str) -> Result<(), Error> { + let head = self.repo.head()?; + let head_commit = head.peel_to_commit()?; + let _ = self.repo.branch(branch_name, &head_commit, false); + Ok(()) + } + + pub fn checkout(&self, branch_name: &str) -> Result<(), Error> { + let branch = match self.repo.find_branch(branch_name, git2::BranchType::Local) { + Ok(branch) => branch, + Err(_) => { + let head_commit = self.repo.head()?.peel_to_commit()?; + self.repo.branch(branch_name, &head_commit, false)? + } + }; + + self.repo + .set_head(branch.into_reference().name().unwrap())?; + self.repo + .checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?; + Ok(()) + } } impl Into<git2::Repository> for GitRepository { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a09bd71..df2abb7 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -34,7 +34,7 @@ pub fn blob_to_string(blob: Vec<u8>) -> String { #[derive(Debug, Clone)] pub struct TestAppState { - archive: Archive, + pub archive: Archive, } impl Global for TestAppState { @@ -44,9 +44,6 @@ impl Global for TestAppState { fn db(&self) -> &db::DatabaseConnection { unimplemented!() } - fn archive_path(&self) -> &PathBuf { - unimplemented!() - } } pub async fn initialize_app( @@ -61,7 +58,7 @@ pub async fn initialize_app( pub fn initialize_archive(archive_type: ArchiveType) -> Result<tempfile::TempDir> { match initialize_archive_without_bare(archive_type) { Ok(td) => { - if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) { + if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) { return Err(err); } Ok(td) From ba75f361781f9e53d223caca785decff2323c7aa Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Wed, 1 Jan 2025 22:19:03 +0100 Subject: [PATCH 07/13] refactor: Moved stelae git logic to src/server/api/stelae folder --- src/server/api/mod.rs | 1 + src/server/api/routes.rs | 83 ++++------------------------ src/server/api/state.rs | 8 --- src/server/api/stelae/mod.rs | 68 +++++++++++++++++++++++ src/server/api/stelae/request/mod.rs | 9 +++ src/server/app.rs | 6 +- 6 files changed, 90 insertions(+), 85 deletions(-) create mode 100644 src/server/api/stelae/mod.rs create mode 100644 src/server/api/stelae/request/mod.rs diff --git a/src/server/api/mod.rs b/src/server/api/mod.rs index 5349f87..7cc2307 100644 --- a/src/server/api/mod.rs +++ b/src/server/api/mod.rs @@ -2,4 +2,5 @@ pub mod routes; pub mod serve; pub mod state; +pub mod stelae; pub mod versions; diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs index 5a37704..15abc40 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -3,6 +3,7 @@ clippy::exit, reason = "We exit with 1 error code on any application errors" )] +use std::sync::Arc; use std::{process, sync::OnceLock}; use crate::server::api::state; @@ -11,18 +12,11 @@ use actix_service::ServiceFactory; use actix_web::{ body::MessageBody, dev::{ServiceRequest, ServiceResponse}, - guard, route, web, App, Error, HttpResponse, Responder, Scope, + guard, web, App, Error, Scope, }; -use serde::Deserialize; -use super::state::App as AppState; +use super::stelae::get_blob; use super::{serve::serve, state::Global, versions::versions}; -use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; -use crate::utils::http::get_contenttype; -use crate::utils::paths::clean_path; -use git2::{self, ErrorCode}; - -use super::super::errors::HTTPError; /// Name of the header to guard current documents static HEADER_NAME: OnceLock<String> = OnceLock::new(); @@ -80,9 +74,14 @@ pub fn register_app< ) .app_data(web::Data::new(state.clone())); - app = app - .service(web::scope("/_git").service(get_blob)) - .app_data(web::Data::new(state.clone())); + let stelae_data: Arc<dyn Global> = Arc::new(state.clone()); + app = app.app_data(web::Data::new(stelae_data.clone())).service( + web::scope("_stelae").service( + web::resource("/{namespace}/{name}") + .route(web::get().to(get_blob)) + .route(web::head().to(get_blob)), + ), + ); app = register_dynamic_routes(app, state)?; Ok(app) @@ -327,63 +326,3 @@ fn register_dependent_routes( } Ok(()) } - -/// Structure for -#[derive(Debug, Deserialize)] -struct Info { - /// commit of the repo - commitish: String, - /// path of the file - remainder: Option<String>, -} - -/// Return the content in the stelae archive in the `{namespace}/{name}` -/// repo at the `commitish` commit at the `remainder` path. -/// Return 404 if any are not found or there are any errors. -#[route("/{namespace}/{name}", method = "GET", method = "HEAD")] -#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, info))] -#[expect( - clippy::future_not_send, - reason = "We don't worry about git2-rs not implementing `Send` trait" -)] -async fn get_blob( - path: web::Path<(String, String)>, - info: web::Query<Info>, - data: web::Data<AppState>, -) -> impl Responder { - let (namespace, name) = path.into_inner(); - let info_struct: Info = info.into_inner(); - let commitish = info_struct.commitish; - let remainder = info_struct.remainder.unwrap_or_default(); - let archive_path = &data.archive_path; - let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); - let blob_path = clean_path(&remainder); - let contenttype = get_contenttype(&blob_path); - match blob { - Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content), - Err(error) => blob_error_response(&error, &namespace, &name), - } -} - -/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses -#[expect(clippy::wildcard_enum_match_arm, reason = "Allows _ for enum matching")] -#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))] -fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse { - tracing::error!("{error}",); - if let Some(git_error) = error.downcast_ref::<git2::Error>() { - return match git_error.code() { - // TODO: check this is the right error - ErrorCode::NotFound => { - HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist")) - } - _ => HttpResponse::InternalServerError().body("Unexpected Git error"), - }; - } - match error { - // TODO: Obviously it's better to use custom `Error` types - _ if error.to_string() == GIT_REQUEST_NOT_FOUND => { - HttpResponse::NotFound().body(HTTPError::NotFound.to_string()) - } - _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()), - } -} diff --git a/src/server/api/state.rs b/src/server/api/state.rs index b78c7db..172be04 100644 --- a/src/server/api/state.rs +++ b/src/server/api/state.rs @@ -13,8 +13,6 @@ pub trait Global { fn archive(&self) -> &Archive; /// Database connection fn db(&self) -> &db::DatabaseConnection; - /// path to the Stelae archive - fn archive_path(&self) -> &PathBuf; } /// Application state @@ -24,8 +22,6 @@ pub struct App { pub archive: Archive, /// Database connection pub db: db::DatabaseConnection, - /// path to the Stelae archive - pub archive_path: PathBuf, } impl Global for App { @@ -36,10 +32,6 @@ impl Global for App { fn db(&self) -> &db::DatabaseConnection { &self.db } - - fn archive_path(&self) -> &PathBuf { - &self.archive_path - } } /// Repository to serve diff --git a/src/server/api/stelae/mod.rs b/src/server/api/stelae/mod.rs new file mode 100644 index 0000000..5b9b12b --- /dev/null +++ b/src/server/api/stelae/mod.rs @@ -0,0 +1,68 @@ +//! API endpoint for serving git blobs. + +use std::sync::Arc; + +use actix_web::{web, HttpRequest, HttpResponse, Responder}; +use request::StelaeQueryData; + +use super::state::Global; +use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; +use crate::utils::http::get_contenttype; +use crate::utils::paths::clean_path; +use git2::{self, ErrorCode}; + +use super::super::errors::HTTPError; + +/// Module that maps the HTTP web request body to structs. +pub mod request; + +/// Return the content in the stelae archive in the `{namespace}/{name}` +/// repo at the `commitish` commit at the `remainder` path. +/// Return 404 if any are not found or there are any errors. +#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, query))] +#[expect( + clippy::future_not_send, + reason = "We don't worry about git2-rs not implementing `Send` trait" +)] +pub async fn get_blob( + req: HttpRequest, + path: web::Path<(String, String)>, + query: web::Query<StelaeQueryData>, + data: web::Data<Arc<dyn Global>>, +) -> impl Responder { + let (namespace, name) = path.into_inner(); + let query_data: StelaeQueryData = query.into_inner(); + let commitish = query_data.commitish.unwrap_or_default(); + let remainder = query_data.remainder.unwrap_or_default(); + let archive_path = &data.archive().path.clone(); + let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); + let blob_path = clean_path(&remainder); + let contenttype = get_contenttype(&blob_path); + match blob { + Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content), + Err(error) => blob_error_response(&error, &namespace, &name), + } +} + +/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses +#[expect(clippy::wildcard_enum_match_arm, reason = "Allows _ for enum matching")] +#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))] +fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse { + tracing::error!("{error}",); + if let Some(git_error) = error.downcast_ref::<git2::Error>() { + return match git_error.code() { + // TODO: check this is the right error + ErrorCode::NotFound => { + HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist")) + } + _ => HttpResponse::InternalServerError().body("Unexpected Git error"), + }; + } + match error { + // TODO: Obviously it's better to use custom `Error` types + _ if error.to_string() == GIT_REQUEST_NOT_FOUND => { + HttpResponse::NotFound().body(HTTPError::NotFound.to_string()) + } + _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()), + } +} diff --git a/src/server/api/stelae/request/mod.rs b/src/server/api/stelae/request/mod.rs new file mode 100644 index 0000000..d321a0f --- /dev/null +++ b/src/server/api/stelae/request/mod.rs @@ -0,0 +1,9 @@ +use serde::Deserialize; +/// Structure for +#[derive(Debug, Deserialize)] +pub struct StelaeQueryData { + /// commit of the repo + pub commitish: Option<String>, + /// path of the file + pub remainder: Option<String>, +} diff --git a/src/server/app.rs b/src/server/app.rs index 7e645a6..ede799a 100644 --- a/src/server/app.rs +++ b/src/server/app.rs @@ -57,11 +57,7 @@ pub async fn serve_archive( } }; - let state = AppState { - archive, - db, - archive_path, - }; + let state = AppState { archive, db }; HttpServer::new(move || { init(&state).unwrap_or_else(|err| { From 83eb359e6fbd8244a4254128fc28cf075bded287 Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Wed, 1 Jan 2025 22:19:45 +0100 Subject: [PATCH 08/13] docs: Added changes to CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8bf94..78e2be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,18 @@ and this project adheres to a _modified_ form of _[Semantic Versioning][semver]_ ### Added +- Added tests fot `stelae git` ([64]) + ### Changed +- Merged `stelae git` and `stelae serve` into single command ([64]) + ### Fixed ### Removed +[64]: https://github.com/openlawlibrary/stelae/pull/64 + ## [0.3.2] ### Added From e496668b5a8b882f04755c002776da529497e27a Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Wed, 1 Jan 2025 22:55:06 +0100 Subject: [PATCH 09/13] style: Not to use Arc in sending Data to endpoint --- src/server/api/routes.rs | 4 +--- src/server/api/stelae/mod.rs | 7 +++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs index 15abc40..9a7c606 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -3,7 +3,6 @@ clippy::exit, reason = "We exit with 1 error code on any application errors" )] -use std::sync::Arc; use std::{process, sync::OnceLock}; use crate::server::api::state; @@ -74,8 +73,7 @@ pub fn register_app< ) .app_data(web::Data::new(state.clone())); - let stelae_data: Arc<dyn Global> = Arc::new(state.clone()); - app = app.app_data(web::Data::new(stelae_data.clone())).service( + app = app.app_data(web::Data::new(state.archive().path.clone())).service( web::scope("_stelae").service( web::resource("/{namespace}/{name}") .route(web::get().to(get_blob)) diff --git a/src/server/api/stelae/mod.rs b/src/server/api/stelae/mod.rs index 5b9b12b..d405694 100644 --- a/src/server/api/stelae/mod.rs +++ b/src/server/api/stelae/mod.rs @@ -1,11 +1,10 @@ //! API endpoint for serving git blobs. -use std::sync::Arc; +use std::path::PathBuf; use actix_web::{web, HttpRequest, HttpResponse, Responder}; use request::StelaeQueryData; -use super::state::Global; use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; use crate::utils::http::get_contenttype; use crate::utils::paths::clean_path; @@ -28,13 +27,13 @@ pub async fn get_blob( req: HttpRequest, path: web::Path<(String, String)>, query: web::Query<StelaeQueryData>, - data: web::Data<Arc<dyn Global>>, + data: web::Data<PathBuf>, ) -> impl Responder { let (namespace, name) = path.into_inner(); let query_data: StelaeQueryData = query.into_inner(); let commitish = query_data.commitish.unwrap_or_default(); let remainder = query_data.remainder.unwrap_or_default(); - let archive_path = &data.archive().path.clone(); + let archive_path = &data; let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); let blob_path = clean_path(&remainder); let contenttype = get_contenttype(&blob_path); From b043e2355448085f54562bf1e41a99bd7070f169 Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Wed, 1 Jan 2025 22:55:18 +0100 Subject: [PATCH 10/13] docs: Added changes to CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e2be3..c2590b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to a _modified_ form of _[Semantic Versioning][semver]_ ### Fixed +- git serve now support commitish that contains / in name ([64]) + ### Removed [64]: https://github.com/openlawlibrary/stelae/pull/64 From fb256c50e1577888dc9c420016818b45a9bb0596 Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Thu, 2 Jan 2025 11:07:45 +0100 Subject: [PATCH 11/13] refactor: run cargo fmt command --- src/server/api/routes.rs | 16 +++++++++------- tests/common/mod.rs | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs index 9a7c606..d6d30ea 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -73,13 +73,15 @@ pub fn register_app< ) .app_data(web::Data::new(state.clone())); - app = app.app_data(web::Data::new(state.archive().path.clone())).service( - web::scope("_stelae").service( - web::resource("/{namespace}/{name}") - .route(web::get().to(get_blob)) - .route(web::head().to(get_blob)), - ), - ); + app = app + .app_data(web::Data::new(state.archive().path.clone())) + .service( + web::scope("_stelae").service( + web::resource("/{namespace}/{name}") + .route(web::get().to(get_blob)) + .route(web::head().to(get_blob)), + ), + ); app = register_dynamic_routes(app, state)?; Ok(app) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index df2abb7..327d09f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -58,7 +58,7 @@ pub async fn initialize_app( pub fn initialize_archive(archive_type: ArchiveType) -> Result<tempfile::TempDir> { match initialize_archive_without_bare(archive_type) { Ok(td) => { - if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) { + if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) { return Err(err); } Ok(td) From ea02fe054fa75e037a1b124793935457fca8f131 Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Thu, 9 Jan 2025 19:56:41 +0100 Subject: [PATCH 12/13] docs: Completed documentation for StelaeQueryData --- src/server/api/stelae/mod.rs | 2 +- src/server/api/stelae/request/mod.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/server/api/stelae/mod.rs b/src/server/api/stelae/mod.rs index d405694..3ba1362 100644 --- a/src/server/api/stelae/mod.rs +++ b/src/server/api/stelae/mod.rs @@ -31,7 +31,7 @@ pub async fn get_blob( ) -> impl Responder { let (namespace, name) = path.into_inner(); let query_data: StelaeQueryData = query.into_inner(); - let commitish = query_data.commitish.unwrap_or_default(); + let commitish = query_data.commitish; let remainder = query_data.remainder.unwrap_or_default(); let archive_path = &data; let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); diff --git a/src/server/api/stelae/request/mod.rs b/src/server/api/stelae/request/mod.rs index d321a0f..97d7e0a 100644 --- a/src/server/api/stelae/request/mod.rs +++ b/src/server/api/stelae/request/mod.rs @@ -1,9 +1,9 @@ use serde::Deserialize; -/// Structure for +/// Structure for passing query parameters for stelae endpoint #[derive(Debug, Deserialize)] pub struct StelaeQueryData { - /// commit of the repo - pub commitish: Option<String>, - /// path of the file + /// commit (or reference) to the repo. Can pass in `HEAD`, a branch ref (e.g. main), or a commit SHA. + pub commitish: String, + /// path of the file (e.g. \us\ca\cities\san-mateo\index.html). If nothing is passed by default it will look for index.html pub remainder: Option<String>, } From 7b51d53875fe08c8ea8388ce87cbef8745b583ba Mon Sep 17 00:00:00 2001 From: Bojan Galic <bojan.galic99@gmail.com> Date: Thu, 9 Jan 2025 19:59:23 +0100 Subject: [PATCH 13/13] test: Added additional tests and moved steale test to its own directory --- tests/api/archive_basic_test.rs | 304 +-------------- tests/api/mod.rs | 1 + tests/api/stelae/mod.rs | 64 +++ tests/api/stelae/stelae_basic_test.rs | 452 ++++++++++++++++++++++ tests/api/stelae/stelae_multihost_test.rs | 43 ++ 5 files changed, 561 insertions(+), 303 deletions(-) create mode 100644 tests/api/stelae/mod.rs create mode 100644 tests/api/stelae/stelae_basic_test.rs create mode 100644 tests/api/stelae/stelae_multihost_test.rs diff --git a/tests/api/archive_basic_test.rs b/tests/api/archive_basic_test.rs index 3955d92..4ad7ff7 100644 --- a/tests/api/archive_basic_test.rs +++ b/tests/api/archive_basic_test.rs @@ -1,11 +1,6 @@ use crate::archive_testtools::config::{ArchiveType, Jurisdiction}; -use crate::archive_testtools::get_repository; use crate::common::{self}; -use actix_http::Request; -use actix_service::Service; -use actix_web::body::MessageBody; -use actix_web::dev::ServiceResponse; -use actix_web::{test, Error}; +use actix_web::test; #[actix_web::test] async fn test_resolve_law_html_request_with_full_path_expect_success() { @@ -194,300 +189,3 @@ async fn get_law_pdf_request_with_incorrect_path_expect_not_found() { let expected = true; assert_eq!(actual, expected); } - -#[actix_web::test] -async fn test_resolve_root_stele_all_repositories_request_with_full_path_expect_success() { - let archive_path = - common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); - let app = common::initialize_app(archive_path.path()).await; - - let mut path = archive_path.path().to_path_buf(); - path.push("test_org"); - - // let git_repo = get_repository(&path, "law-html"); - // let _ = git_repo.create_branch("test_branch"); - println!("Testing law-html"); - test_paths( - "law-html", - vec!["", "a/", "a/b/", "a/d/", "a/b/c.html", "a/b/c/"], - "HEAD", - &app, - true, - ) - .await; - - println!("Testing law-other"); - test_paths( - "law-other", - vec![ - "", - "example.json", - "a/", - "a/e/_doc/f/", - "a/d/", - "a/b/", - "a/b/c.html", - "a/_doc/e/", - "_prefix/", - "_prefix/a/", - ], - "HEAD", - &app, - true, - ) - .await; - - println!("Testing law-pdf"); - test_paths( - "law-pdf", - vec!["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"], - "HEAD", - &app, - true, - ) - .await; - - println!("Testing law-rdf"); - test_paths( - "law-rdf", - vec![ - "index.rdf", - "a/index.rdf", - "a/b/index.rdf", - "a/d/index.rdf", - "a/b/c.rdf", - "a/b/c/index.rdf", - ], - "HEAD", - &app, - true, - ) - .await; - - println!("Testing law-xml"); - test_paths( - "law-xml", - vec![ - "index.xml", - "a/index.xml", - "a/b/index.xml", - "a/d/index.xml", - "a/b/c.xml", - "a/b/c/index.xml", - ], - "HEAD", - &app, - true, - ) - .await; - - println!("Testing law-xml-codified"); - test_paths( - "law-xml-codified", - vec!["index.xml", "e/index.xml", "e/f/index.xml", "e/g/index.xml"], - "master", - &app, - true, - ) - .await; -} - -#[actix_web::test] -async fn test_resolve_root_stele_all_repositories_request_with_incorrect_path_expect_client_error() -{ - let archive_path = - common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); - let app = common::initialize_app(archive_path.path()).await; - - let mut path = archive_path.path().to_path_buf(); - path.push("test_org"); - - println!("Testing law-html"); - test_paths( - "law-html", - vec!["a/b/c/d", "a/index.css"], - "HEAD", - &app, - false, - ) - .await; - - println!("Testing law-other"); - test_paths( - "law-other", - vec!["a/b/c/d", "example1.json"], - "HEAD", - &app, - false, - ) - .await; - - println!("Testing law-pdf"); - test_paths( - "law-pdf", - vec!["/example1.pdf", "/c/example.pdf"], - "HEAD", - &app, - false, - ) - .await; - - println!("Testing law-rdf"); - test_paths( - "law-rdf", - vec!["index1.rdf", "z/index.rdf"], - "HEAD", - &app, - false, - ) - .await; - - println!("Testing law-xml"); - test_paths( - "law-xml", - vec!["index1.xml", "t/index.xml"], - "HEAD", - &app, - false, - ) - .await; - - println!("Testing law-xml-codified"); - test_paths( - "law-xml-codified", - vec!["index1.xml", "x/index.xml"], - "HEAD", - &app, - false, - ) - .await; -} - -async fn test_paths( - repo_name: &str, - file_paths: Vec<&str>, - branch_name: &str, - app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>, - expected: bool, -) { - for request_uri in file_paths { - let req = test::TestRequest::get() - .uri(&format!( - "/_stelae/test_org/{}?commitish={}&remainder={}", - repo_name, branch_name, request_uri - )) - .to_request(); - let resp = test::call_service(&app, req).await; - let actual = if expected { - resp.status().is_success() - } else { - resp.status().is_client_error() - }; - let expected = true; - assert_eq!(actual, expected); - } -} - -#[actix_web::test] -async fn test_resolve_root_stele_law_html_different_files_with_different_branches() { - let archive_path = - common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); - let app = common::initialize_app(archive_path.path()).await; - - let mut path = archive_path.path().to_path_buf(); - path.push("test_org"); - let git_repo = get_repository(&path, "law-html"); - path.push("law-html"); - let _ = git_repo.create_branch("test_branch"); - - let _ = git_repo.checkout("master"); - let _ = git_repo.add_file(&path, "test.txt", "Content for master branch"); - let _ = git_repo.commit(None, "Adding data for master branch"); - - let _ = git_repo.checkout("test_branch"); - let _ = git_repo.add_file(&path, "test1.txt", "Content for test branch"); - let _ = git_repo.commit(None, "Adding data for master branch"); - - let req = test::TestRequest::get() - .uri(&format!( - "/_stelae/test_org/law-html?commitish=master&remainder=/test.txt" - )) - .to_request(); - let resp = test::call_service(&app, req).await; - let actual = resp.status().is_success(); - assert_eq!(actual, true); - - let req = test::TestRequest::get() - .uri(&format!( - "/_stelae/test_org/law-html?commitish=master&remainder=/test1.txt" - )) - .to_request(); - let resp = test::call_service(&app, req).await; - let actual = resp.status().is_success(); - assert_eq!(actual, false); - - let req = test::TestRequest::get() - .uri(&format!( - "/_stelae/test_org/law-html?commitish=test_branch&remainder=/test.txt" - )) - .to_request(); - let resp = test::call_service(&app, req).await; - let actual = resp.status().is_success(); - assert_eq!(actual, false); - - let req = test::TestRequest::get() - .uri(&format!( - "/_stelae/test_org/law-html?commitish=test_branch&remainder=/test1.txt" - )) - .to_request(); - let resp = test::call_service(&app, req).await; - let actual = resp.status().is_success(); - assert_eq!(actual, true); -} - -#[actix_web::test] -async fn test_resolve_root_stele_law_html_file_content_with_different_branches_expect_success() { - let archive_path = - common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); - let app = common::initialize_app(archive_path.path()).await; - - let mut path = archive_path.path().to_path_buf(); - path.push("test_org"); - let git_repo = get_repository(&path, "law-html"); - path.push("law-html"); - - let _ = git_repo.create_branch("test_branch"); - - let _ = git_repo.checkout("master"); - let _ = git_repo.add_file(&path, "test.txt", "Content for master branch"); - let _ = git_repo.commit(None, "Adding data for master branch"); - - let _ = git_repo.checkout("test_branch"); - let _ = git_repo.add_file(&path, "test.txt", "Content for test branch"); - let _ = git_repo.commit(None, "Adding data for test branch"); - - let req = test::TestRequest::get() - .uri(&format!( - "/_stelae/test_org/law-html?commitish=master&remainder=/test.txt" - )) - .to_request(); - let actual = test::call_and_read_body(&app, req).await; - let expected = "Content for master branch"; - assert!( - common::blob_to_string(actual.to_vec()).starts_with(expected), - "doesn't start with {expected}" - ); - - let req = test::TestRequest::get() - .uri(&format!( - "/_stelae/test_org/law-html?commitish=test_branch&remainder=test.txt" - )) - .to_request(); - let actual = test::call_and_read_body(&app, req).await; - let expected = "Content for test branch"; - println!("{}", common::blob_to_string(actual.to_vec())); - assert!( - common::blob_to_string(actual.to_vec()).starts_with(expected), - "doesn't start with {expected}" - ); -} diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 6983843..7dee2c4 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -1,3 +1,4 @@ mod archive_basic_test; mod archive_multihost_test; mod archive_multijursidiction_test; +mod stelae; diff --git a/tests/api/stelae/mod.rs b/tests/api/stelae/mod.rs new file mode 100644 index 0000000..00100e0 --- /dev/null +++ b/tests/api/stelae/mod.rs @@ -0,0 +1,64 @@ +use actix_http::{Method, Request}; +use actix_service::Service; +use actix_web::body::MessageBody; +use actix_web::dev::ServiceResponse; +use actix_web::{test, Error}; +mod stelae_basic_test; +mod stelae_multihost_test; + +/// Helper method which test all `fille_paths`` in `org_name`/`repo_name` repository on `branch_name`` branch with `expected` result +async fn test_stelae_paths( + org_name: &str, + repo_name: &str, + file_paths: Vec<&str>, + branch_name: &str, + app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>, + expected: bool, +) { + for file_path in file_paths { + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/{}/{}?commitish={}&remainder={}", + org_name, repo_name, branch_name, file_path + )) + .to_request(); + + let resp = test::call_service(&app, req).await; + let actual = if expected { + resp.status().is_success() + } else { + resp.status().is_client_error() + }; + let expected = true; + assert_eq!(actual, expected); + } +} + +/// Helper method which test all `fille_paths`` in `org_name`/`repo_name` repository on `branch_name`` branch with `expected` result +async fn test_stelae_paths_with_head_method( + org_name: &str, + repo_name: &str, + file_paths: Vec<&str>, + branch_name: &str, + app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>, + expected: bool, +) { + for file_path in file_paths { + let req = test::TestRequest::default() + .method(Method::HEAD) + .uri(&format!( + "/_stelae/{}/{}?commitish={}&remainder={}", + org_name, repo_name, branch_name, file_path + )) + .to_request(); + + let resp = test::call_service(&app, req).await; + let actual = if expected { + resp.status().is_success() + } else { + resp.status().is_client_error() + }; + let expected = true; + assert_eq!(actual, expected); + } +} diff --git a/tests/api/stelae/stelae_basic_test.rs b/tests/api/stelae/stelae_basic_test.rs new file mode 100644 index 0000000..0db46c1 --- /dev/null +++ b/tests/api/stelae/stelae_basic_test.rs @@ -0,0 +1,452 @@ +use actix_web::test; + +use crate::api::stelae::test_stelae_paths; +use crate::archive_testtools::config::{ArchiveType, Jurisdiction}; +use crate::archive_testtools::get_repository; +use crate::common; + +use super::test_stelae_paths_with_head_method; + +#[actix_web::test] +async fn test_stele_api_on_all_repositories_with_full_path_expect_success() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + test_stelae_paths( + "test_org", + "law-html", + vec!["", "a/", "a/b/", "a/d/", "a/b/c.html", "a/b/c/"], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths( + "test_org", + "law-other", + vec![ + "", + "example.json", + "a/", + "a/e/_doc/f/", + "a/d/", + "a/b/", + "a/b/c.html", + "a/_doc/e/", + "_prefix/", + "_prefix/a/", + ], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths( + "test_org", + "law-pdf", + vec!["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths( + "test_org", + "law-rdf", + vec![ + "index.rdf", + "a/index.rdf", + "a/b/index.rdf", + "a/d/index.rdf", + "a/b/c.rdf", + "a/b/c/index.rdf", + ], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths( + "test_org", + "law-xml", + vec![ + "index.xml", + "a/index.xml", + "a/b/index.xml", + "a/d/index.xml", + "a/b/c.xml", + "a/b/c/index.xml", + ], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths( + "test_org", + "law-xml-codified", + vec!["index.xml", "e/index.xml", "e/f/index.xml", "e/g/index.xml"], + "HEAD", + &app, + true, + ) + .await; +} + +#[actix_web::test] +async fn test_stele_api_on_all_repositories_with_head_method_expect_success() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + test_stelae_paths_with_head_method( + "test_org", + "law-html", + vec!["", "a/", "a/b/", "a/d/", "a/b/c.html", "a/b/c/"], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths_with_head_method( + "test_org", + "law-other", + vec![ + "", + "example.json", + "a/", + "a/e/_doc/f/", + "a/d/", + "a/b/", + "a/b/c.html", + "a/_doc/e/", + "_prefix/", + "_prefix/a/", + ], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths_with_head_method( + "test_org", + "law-pdf", + vec!["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths_with_head_method( + "test_org", + "law-rdf", + vec![ + "index.rdf", + "a/index.rdf", + "a/b/index.rdf", + "a/d/index.rdf", + "a/b/c.rdf", + "a/b/c/index.rdf", + ], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths_with_head_method( + "test_org", + "law-xml", + vec![ + "index.xml", + "a/index.xml", + "a/b/index.xml", + "a/d/index.xml", + "a/b/c.xml", + "a/b/c/index.xml", + ], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths_with_head_method( + "test_org", + "law-xml-codified", + vec!["index.xml", "e/index.xml", "e/f/index.xml", "e/g/index.xml"], + "HEAD", + &app, + true, + ) + .await; +} + +#[actix_web::test] +async fn test_stele_api_on_law_html_repository_with_missing_branch_name_expect_client_error() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + test_stelae_paths( + "test_org", + "law-html", + vec!["", "a/index.html"], + "", + &app, + false, + ) + .await; +} + +#[actix_web::test] +async fn test_stele_api_on_law_html_repository_with_invalid_branch_name_expect_client_error() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + test_stelae_paths( + "test_org", + "law-html", + vec!["", "a/index.html"], + "notExistingBranch", + &app, + false, + ) + .await; +} + +#[actix_web::test] +async fn test_stele_api_on_law_html_repository_with_invalid_org_name_expect_client_error() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + test_stelae_paths( + "not_test_org", + "law-html", + vec!["", "a/index.html"], + "HEAD", + &app, + false, + ) + .await; +} + +#[actix_web::test] +async fn test_stele_api_on_law_html_repository_with_invalid_repo_name_expect_client_error() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + test_stelae_paths( + "test_org", + "not_law-html", + vec!["", "a/index.html"], + "HEAD", + &app, + false, + ) + .await; +} + +#[actix_web::test] +async fn test_stele_api_on_law_html_repository_with_incorrect_paths_expect_client_error() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + test_stelae_paths( + "test_org", + "law-html", + vec!["a/b/c/d", "a/index.css"], + "HEAD", + &app, + false, + ) + .await; +} + +#[actix_web::test] +async fn test_stele_api_on_law_html_repository_with_different_files_on_different_branches_expect_success( +) { + let archive_path = + common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + let mut path = archive_path.path().to_path_buf(); + path.push("test_org"); + let git_repo = get_repository(&path, "law-html"); + path.push("law-html"); + let _ = git_repo.create_branch("test_branch"); + let _ = git_repo.create_branch("default_branch"); + + let _ = git_repo.checkout("default_branch"); + let _ = git_repo.add_file(&path, "test.txt", "Content for default branch"); + let _ = git_repo.commit(None, "Adding data for default branch"); + + let _ = git_repo.checkout("test_branch"); + let _ = git_repo.add_file(&path, "test1.txt", "Content for test branch"); + let _ = git_repo.commit(None, "Adding data for test branch"); + + test_stelae_paths( + "test_org", + "law-html", + vec!["/test.txt"], + "default_branch", + &app, + true, + ) + .await; + + test_stelae_paths( + "test_org", + "law-html", + vec!["/test1.txt"], + "default_branch", + &app, + false, + ) + .await; + + test_stelae_paths( + "test_org", + "law-html", + vec!["/test.txt"], + "test_branch", + &app, + false, + ) + .await; + + test_stelae_paths( + "test_org", + "law-html", + vec!["/test1.txt"], + "test_branch", + &app, + true, + ) + .await; +} + +#[actix_web::test] +async fn test_stele_api_with_same_file_on_different_branches_expect_different_file_content_on_different_branches( +) { + let archive_path = + common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + let mut path = archive_path.path().to_path_buf(); + path.push("test_org"); + let git_repo = get_repository(&path, "law-html"); + path.push("law-html"); + + let _ = git_repo.create_branch("test_branch"); + let _ = git_repo.create_branch("default_branch"); + + let _ = git_repo.checkout("default_branch"); + let _ = git_repo.add_file(&path, "test.txt", "Content for default branch"); + let _ = git_repo.commit(None, "Adding data for default branch"); + + let _ = git_repo.checkout("test_branch"); + let _ = git_repo.add_file(&path, "test.txt", "Content for test branch"); + let _ = git_repo.commit(None, "Adding data for test branch"); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish=default_branch&remainder=/test.txt" + )) + .to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = "Content for default branch"; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish=test_branch&remainder=test.txt" + )) + .to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = "Content for test branch"; + println!("{}", common::blob_to_string(actual.to_vec())); + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); +} + +#[actix_web::test] +async fn test_stelae_api_where_branch_contains_slashs_expect_resolved_content() { + let archive_path = + common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + let mut path = archive_path.path().to_path_buf(); + path.push("test_org"); + let git_repo = get_repository(&path, "law-html"); + path.push("law-html"); + let branch_name = "test/branch/with/slash"; + let _ = git_repo.create_branch(branch_name); + + let _ = git_repo.checkout(branch_name); + let _ = git_repo.add_file(&path, "test.txt", "Content for test branch"); + let _ = git_repo.commit(None, "Adding data for test branch"); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish={}&remainder=/test.txt", + branch_name + )) + .to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = "Content for test branch"; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); +} + +#[actix_web::test] +async fn test_stelae_api_where_branch_is_commit_sha_expect_resolved_content() { + let archive_path = + common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + let mut path = archive_path.path().to_path_buf(); + path.push("test_org"); + let git_repo = get_repository(&path, "law-html"); + path.push("law-html"); + let branch_name = "test/branch/with/slash"; + let _ = git_repo.create_branch(branch_name); + + let _ = git_repo.checkout(branch_name); + let _ = git_repo.add_file(&path, "test.txt", "Content for test branch"); + let commit_hash = git_repo.commit(None, "Adding data for test branch"); + let sha_string = commit_hash.unwrap().to_string(); + + let req = test::TestRequest::get() + .uri(&format!( + "/_stelae/test_org/law-html?commitish={}&remainder=/test.txt", + sha_string + )) + .to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = "Content for test branch"; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); +} diff --git a/tests/api/stelae/stelae_multihost_test.rs b/tests/api/stelae/stelae_multihost_test.rs new file mode 100644 index 0000000..3822fff --- /dev/null +++ b/tests/api/stelae/stelae_multihost_test.rs @@ -0,0 +1,43 @@ +//# def test_<method/code under test>_where_<conditions/inputs/state>_expect_<result> + +use crate::{archive_testtools::config::ArchiveType, common}; + +use super::test_stelae_paths; + +#[actix_web::test] +async fn test_stelae_api_with_multiple_repositories_expect_success() { + let archive_path = common::initialize_archive(ArchiveType::Multihost).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + test_stelae_paths( + "stele_1", + "law-html", + vec!["/a/b/c.html"], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths( + "stele_1_1", + "law-pdf", + vec!["/a/b/example.pdf"], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths( + "stele_1_2", + "law-xml", + vec!["/a/b/c/index.xml"], + "HEAD", + &app, + true, + ) + .await; + + test_stelae_paths("stele_2", "law-rdf", vec!["/a/b/c.rdf"], "HEAD", &app, true).await; +}