fix: preserve backend product authority boundaries
This commit is contained in:
@@ -173,7 +173,7 @@ impl BackendWorkspaceProductClient {
|
|||||||
}
|
}
|
||||||
self.send_json(
|
self.send_json(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
"/tickets/relations/search",
|
"/ticket-relations/query",
|
||||||
Some(&Query { ticket, kind }),
|
Some(&Query { ticket, kind }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -568,7 +568,7 @@ impl TicketBackend for BackendWorkspaceProductClient {
|
|||||||
}
|
}
|
||||||
self.send_json(
|
self.send_json(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
"/tickets/orchestration-plans/search",
|
"/ticket-orchestration-plans/query",
|
||||||
Some(&Query { ticket, kind }),
|
Some(&Query { ticket, kind }),
|
||||||
)
|
)
|
||||||
.map_err(ticket_client_error)
|
.map_err(ticket_client_error)
|
||||||
@@ -731,6 +731,42 @@ mod tests {
|
|||||||
handle.join().unwrap();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ticket_relation_query_uses_workspace_scoped_backend_route() {
|
||||||
|
let (base_url, request, handle) = one_response_server("200 OK", "[]");
|
||||||
|
let client = BackendWorkspaceProductClient::new(base_url, "workspace-a").unwrap();
|
||||||
|
|
||||||
|
let relations = client
|
||||||
|
.query_ticket_relations(
|
||||||
|
Some(&TicketIdOrSlug::Query("T-1".to_string())),
|
||||||
|
Some(TicketRelationKind::Related),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(relations.is_empty());
|
||||||
|
let request = request.recv().unwrap();
|
||||||
|
assert!(request.starts_with("POST /api/w/workspace-a/ticket-relations/query "));
|
||||||
|
assert!(request.contains("\"ticket\":{\"Query\":\"T-1\"}"));
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn orchestration_plan_query_uses_workspace_scoped_backend_route() {
|
||||||
|
let (base_url, request, handle) = one_response_server("200 OK", "[]");
|
||||||
|
let client = BackendWorkspaceProductClient::new(base_url, "workspace-a").unwrap();
|
||||||
|
|
||||||
|
let records = TicketBackend::query_orchestration_plan_records(&client, None, None).unwrap();
|
||||||
|
|
||||||
|
assert!(records.is_empty());
|
||||||
|
assert!(
|
||||||
|
request
|
||||||
|
.recv()
|
||||||
|
.unwrap()
|
||||||
|
.starts_with("POST /api/w/workspace-a/ticket-orchestration-plans/query ")
|
||||||
|
);
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_intake_launch_uses_backend_options_and_workspace_worker_route() {
|
fn ticket_intake_launch_uses_backend_options_and_workspace_worker_route() {
|
||||||
let (base_url, requests, handle) = response_sequence_server(vec![
|
let (base_url, requests, handle) = response_sequence_server(vec![
|
||||||
|
|||||||
@@ -3672,13 +3672,10 @@ impl ticket::TicketTargetAuthority for WorkspaceTicketTargetAuthority {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn browser_ticket_backend(api: &WorkspaceApi) -> Result<SqliteTicketBackend> {
|
fn browser_ticket_backend(api: &WorkspaceApi) -> Result<SqliteTicketBackend> {
|
||||||
let config = ticket::config::TicketConfig::load_workspace(&api.config.workspace_root)
|
|
||||||
.map_err(|error| Error::Config(format!("load Ticket workspace settings: {error}")))?;
|
|
||||||
Ok(SqliteTicketBackend::open_verified(
|
Ok(SqliteTicketBackend::open_verified(
|
||||||
api.config.database_path.clone(),
|
api.config.database_path.clone(),
|
||||||
api.config.workspace_id.clone(),
|
api.config.workspace_id.clone(),
|
||||||
)?
|
)?
|
||||||
.with_record_language(config.ticket_record_language())
|
|
||||||
.with_target_authority(Arc::new(WorkspaceTicketTargetAuthority {
|
.with_target_authority(Arc::new(WorkspaceTicketTargetAuthority {
|
||||||
api: api.clone(),
|
api: api.clone(),
|
||||||
})))
|
})))
|
||||||
@@ -3849,14 +3846,11 @@ async fn execute_ticket_rest_operation(
|
|||||||
mut operation: TicketBackendOperation,
|
mut operation: TicketBackendOperation,
|
||||||
) -> ApiResult<TicketBackendOperationResult> {
|
) -> ApiResult<TicketBackendOperationResult> {
|
||||||
validate_workspace_scope(api, workspace_id)?;
|
validate_workspace_scope(api, workspace_id)?;
|
||||||
let config = ticket::config::TicketConfig::load_workspace(&api.config.workspace_root)
|
|
||||||
.map_err(|error| Error::Config(format!("load Ticket workspace settings: {error}")))?;
|
|
||||||
let mut backend = SqliteTicketBackend::open_verified(
|
let mut backend = SqliteTicketBackend::open_verified(
|
||||||
api.config.database_path.clone(),
|
api.config.database_path.clone(),
|
||||||
api.config.workspace_id.clone(),
|
api.config.workspace_id.clone(),
|
||||||
)
|
)
|
||||||
.map_err(Error::from)?
|
.map_err(Error::from)?
|
||||||
.with_record_language(config.ticket_record_language())
|
|
||||||
.with_target_authority(Arc::new(WorkspaceTicketTargetAuthority {
|
.with_target_authority(Arc::new(WorkspaceTicketTargetAuthority {
|
||||||
api: api.clone(),
|
api: api.clone(),
|
||||||
}));
|
}));
|
||||||
@@ -16285,6 +16279,12 @@ mod tests {
|
|||||||
async fn ticket_browser_endpoints_mutate_typed_backend_and_return_thread() {
|
async fn ticket_browser_endpoints_mutate_typed_backend_and_return_thread() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
init_clean_git_workspace(dir.path());
|
init_clean_git_workspace(dir.path());
|
||||||
|
fs::create_dir_all(dir.path().join(".yoi")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join(".yoi/workspace.toml"),
|
||||||
|
"this is not valid workspace config",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let api = test_api(dir.path()).await;
|
let api = test_api(dir.path()).await;
|
||||||
let ticket_ref = browser_ticket_backend(&api)
|
let ticket_ref = browser_ticket_backend(&api)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
+55
-10
@@ -214,22 +214,15 @@ struct TargetSelection {
|
|||||||
workspace_id: Option<String>,
|
workspace_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TargetSelection {
|
|
||||||
fn explicit_backend(&self) -> bool {
|
|
||||||
self.backend_url.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_tui_target<R: CliConnectionResolver + ?Sized>(
|
fn resolve_tui_target<R: CliConnectionResolver + ?Sized>(
|
||||||
connection_resolver: &R,
|
connection_resolver: &R,
|
||||||
command: CliCommand,
|
command: CliCommand,
|
||||||
selection: &TargetSelection,
|
selection: &TargetSelection,
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
) -> Result<Box<dyn Target>, ParseError> {
|
) -> Result<Box<dyn Target>, ParseError> {
|
||||||
let workspace_id = if selection.explicit_backend() && selection.workspace_id.is_none() {
|
let workspace_id = match selection.workspace_id.clone() {
|
||||||
resolve_workspace_id_from_root(workspace_root)?
|
Some(workspace_id) => Some(workspace_id),
|
||||||
} else {
|
None => resolve_workspace_id_from_root(workspace_root)?,
|
||||||
selection.workspace_id.clone()
|
|
||||||
};
|
};
|
||||||
resolve_connection_aware_cli_connection(
|
resolve_connection_aware_cli_connection(
|
||||||
connection_resolver,
|
connection_resolver,
|
||||||
@@ -1755,6 +1748,28 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DefaultBackendCliConnectionResolver {
|
||||||
|
backend_url: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliConnectionResolver for DefaultBackendCliConnectionResolver {
|
||||||
|
fn resolve_connection(
|
||||||
|
&self,
|
||||||
|
_command: CliCommand,
|
||||||
|
input: CliConnectionInput<'_>,
|
||||||
|
) -> Result<Box<dyn Target>, ParseError> {
|
||||||
|
let workspace_id = match input {
|
||||||
|
CliConnectionInput::DefaultTarget { workspace_id }
|
||||||
|
| CliConnectionInput::BackendTarget { workspace_id, .. } => workspace_id,
|
||||||
|
CliConnectionInput::LocalTarget => return Ok(Box::new(LocalTarget::new())),
|
||||||
|
};
|
||||||
|
Ok(Box::new(BackendTarget::new(
|
||||||
|
self.backend_url,
|
||||||
|
workspace_id.map(str::to_string),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_uses_local_target_for_workers_without_backend_option() {
|
fn parser_uses_local_target_for_workers_without_backend_option() {
|
||||||
let resolver = FixedCliConnectionResolver {
|
let resolver = FixedCliConnectionResolver {
|
||||||
@@ -2136,6 +2151,36 @@ backend = "shared"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_backend_target_inherits_workspace_identity_from_workspace_root() {
|
||||||
|
let workspace = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::create_dir_all(workspace.path().join(".yoi")).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
workspace.path().join(".yoi/workspace.toml"),
|
||||||
|
"workspace_id = \"workspace-from-root\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let resolver = DefaultBackendCliConnectionResolver {
|
||||||
|
backend_url: "http://default-backend.example",
|
||||||
|
};
|
||||||
|
|
||||||
|
let target = resolve_tui_target(
|
||||||
|
&resolver,
|
||||||
|
CliCommand::Ticket,
|
||||||
|
&TargetSelection::default(),
|
||||||
|
workspace.path(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
target.resolve().unwrap(),
|
||||||
|
client::ResolvedTarget::Backend {
|
||||||
|
base_url: "http://default-backend.example".to_string(),
|
||||||
|
workspace_id: "workspace-from-root".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_ticket_subcommand_uses_ticket_mode() {
|
fn parse_ticket_subcommand_uses_ticket_mode() {
|
||||||
match parse_args_from(["ticket", "doctor"]).unwrap() {
|
match parse_args_from(["ticket", "doctor"]).unwrap() {
|
||||||
|
|||||||
Reference in New Issue
Block a user