use std::sync::Arc;
use axum::response::{IntoResponse, Response};
use trusty_common::github_path::{GithubPath, parse_github_path};
use crate::daemon::api::deliverable_routes::fetch_scoped;
use crate::daemon::error::DaemonError;
use crate::daemon::state::DaemonState;
use crate::project::record::Project;
use crate::project::resolver::resolve_project_by_repo_url;
fn github_identity_for_repo_url(repo_url: &str) -> Option<GithubPath> {
if super::is_local_workdir(repo_url) {
let origin = super::inproject::get_origin_url(std::path::Path::new(repo_url))
.inspect_err(|e| {
tracing::warn!(repo_url, "deliverable scope: cannot read git remote: {e}");
})
.ok()
.flatten()?;
parse_github_path(&origin)
} else {
parse_github_path(repo_url)
}
}
fn resolve_project_for_deliverable_scope<'a>(
repo_url: &str,
projects: &'a [Project],
) -> Option<&'a Project> {
if let Some(target) = github_identity_for_repo_url(repo_url)
&& let Some(project) = projects
.iter()
.find(|p| parse_github_path(&p.repo_url).as_ref() == Some(&target))
{
return Some(project);
}
resolve_project_by_repo_url(repo_url, projects)
}
pub(super) async fn validate_deliverable_scope(
state: &Arc<DaemonState>,
repo_url: &str,
deliverable_id: &str,
) -> Result<(), Response> {
let projects = state
.project_registry()
.await
.list()
.await
.unwrap_or_default();
let Some(project) = resolve_project_for_deliverable_scope(repo_url, &projects) else {
return Err(DaemonError::ProjectNotFoundForRepoUrl {
repo_url: repo_url.into(),
}
.into_response());
};
fetch_scoped(state, &project.name, deliverable_id)
.await
.map(|_| ())
.map_err(IntoResponse::into_response)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
use chrono::Utc;
use tempfile::TempDir;
use crate::deliverable::{
Deliverable, DeliverableId, DeliverableKind, DeliverableStatus, EstimationTier,
};
use crate::project::record::Project;
fn state() -> (Arc<DaemonState>, TempDir) {
let dir = tempfile::tempdir().expect("tempdir");
let st = Arc::new(DaemonState::with_root(dir.path().to_path_buf()));
(st, dir)
}
fn project(name: &str, repo_url: &str) -> Project {
Project {
name: name.to_string(),
repo_url: repo_url.to_string(),
default_branch: "main".to_string(),
stack_hint: None,
tags: vec![],
description: None,
gh_user: None,
gh_account: None,
github: None,
commit_name: None,
commit_email: None,
worktree: None,
}
}
fn deliverable(project_name: &str) -> Deliverable {
Deliverable {
id: DeliverableId::new(),
project_name: project_name.to_string(),
name: "sample".into(),
description: String::new(),
kind: DeliverableKind::Feature,
ticket_ref: None,
spec_ref: None,
status: DeliverableStatus::Proposed,
estimated_effort: EstimationTier::M,
created_at: Utc::now(),
target_date: None,
}
}
fn git_repo_with_origin(dir: &std::path::Path, origin_url: &str) {
let init = std::process::Command::new("git")
.args(["init", dir.to_str().expect("path is utf8")])
.output()
.expect("git init");
assert!(init.status.success(), "git init failed");
let remote = std::process::Command::new("git")
.args([
"-C",
dir.to_str().expect("path is utf8"),
"remote",
"add",
"origin",
origin_url,
])
.output()
.expect("git remote add");
assert!(remote.status.success(), "git remote add failed");
}
#[tokio::test]
async fn validate_deliverable_scope_unknown_project_is_404() {
let (st, _g) = state();
let err = validate_deliverable_scope(&st, "https://github.com/org/unregistered", "any-id")
.await
.expect_err("no project to scope against");
assert_eq!(err.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn validate_deliverable_scope_local_path_matches_registered_project() {
let (st, _g) = state();
st.project_registry()
.await
.register(project(
"trusty-tools",
"https://github.com/org/trusty-tools",
))
.await
.expect("register");
let d = deliverable("trusty-tools");
st.deliverable_manager()
.await
.upsert_deliverable(d.clone())
.await
.expect("upsert");
let repo_dir = tempfile::tempdir().expect("repo tempdir");
git_repo_with_origin(repo_dir.path(), "https://github.com/org/trusty-tools.git");
validate_deliverable_scope(
&st,
repo_dir.path().to_str().expect("path is utf8"),
&d.id.to_string(),
)
.await
.expect("local-path repo_url must resolve to the matching registered project");
}
#[tokio::test]
async fn validate_deliverable_scope_ssh_repo_url_matches_https_registered_project() {
let (st, _g) = state();
st.project_registry()
.await
.register(project(
"trusty-tools",
"https://github.com/org/trusty-tools",
))
.await
.expect("register");
let d = deliverable("trusty-tools");
st.deliverable_manager()
.await
.upsert_deliverable(d.clone())
.await
.expect("upsert");
validate_deliverable_scope(
&st,
"git@github.com:org/trusty-tools.git",
&d.id.to_string(),
)
.await
.expect("SSH-form repo_url must resolve to the HTTPS-registered project");
}
#[tokio::test]
async fn validate_deliverable_scope_unknown_deliverable_is_404() {
let (st, _g) = state();
st.project_registry()
.await
.register(project(
"trusty-tools",
"https://github.com/org/trusty-tools",
))
.await
.expect("register");
let err = validate_deliverable_scope(
&st,
"https://github.com/org/trusty-tools",
&DeliverableId::new().to_string(),
)
.await
.expect_err("deliverable does not exist");
assert_eq!(err.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn validate_deliverable_scope_wrong_project_is_404() {
let (st, _g) = state();
st.project_registry()
.await
.register(project(
"trusty-tools",
"https://github.com/org/trusty-tools",
))
.await
.expect("register");
let d = deliverable("some-other-project");
st.deliverable_manager()
.await
.upsert_deliverable(d.clone())
.await
.expect("upsert");
let err = validate_deliverable_scope(
&st,
"https://github.com/org/trusty-tools",
&d.id.to_string(),
)
.await
.expect_err("wrong-project deliverable must 404, not leak");
assert_eq!(err.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn validate_deliverable_scope_valid_id_passes() {
let (st, _g) = state();
st.project_registry()
.await
.register(project(
"trusty-tools",
"https://github.com/org/trusty-tools",
))
.await
.expect("register");
let d = deliverable("trusty-tools");
st.deliverable_manager()
.await
.upsert_deliverable(d.clone())
.await
.expect("upsert");
validate_deliverable_scope(
&st,
"https://github.com/org/trusty-tools",
&d.id.to_string(),
)
.await
.expect("matching project + existing deliverable must pass");
}
}