workspace: add ticket panel workflow
This commit is contained in:
@@ -10,6 +10,10 @@ autobins = false
|
||||
name = "yoi-server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["typescript"]
|
||||
typescript = ["dep:ts-rs"]
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
@@ -33,6 +37,7 @@ worker.workspace = true
|
||||
worker-runtime.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
ts-rs = { version = "12.0.1", optional = true }
|
||||
url.workspace = true
|
||||
uuid = { workspace = true, features = ["v7"] }
|
||||
webauthn-rs = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
print!("{}", yoi_workspace_server::ticket_api_typescript());
|
||||
}
|
||||
@@ -274,6 +274,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
|
||||
updated_at: ticket.meta.updated_at,
|
||||
queued_by: ticket.meta.queued_by,
|
||||
queued_at: ticket.meta.queued_at,
|
||||
assignee: ticket.meta.assignee,
|
||||
repository_id: ticket.meta.repository_id,
|
||||
ref_selector: ticket.meta.ref_selector,
|
||||
risk_flags: ticket.meta.risk_flags,
|
||||
@@ -287,6 +288,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
|
||||
.into_iter()
|
||||
.map(|artifact| artifact.relative_path.display().to_string())
|
||||
.collect(),
|
||||
relations: ticket.relations.into(),
|
||||
resolution: ticket
|
||||
.resolution
|
||||
.map(|resolution| resolution.as_str().to_string()),
|
||||
|
||||
@@ -15,6 +15,8 @@ pub mod memory_staging;
|
||||
pub mod observation;
|
||||
pub mod profile_settings;
|
||||
pub mod records;
|
||||
#[cfg(feature = "typescript")]
|
||||
pub use records::ticket_api_typescript;
|
||||
pub mod repositories;
|
||||
pub mod resource_broker;
|
||||
pub mod server;
|
||||
|
||||
@@ -13,12 +13,14 @@ pub struct ProjectRecordList<T> {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct InvalidProjectRecord {
|
||||
pub label: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketSummary {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
@@ -32,6 +34,17 @@ pub struct TicketSummary {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketListResponse {
|
||||
pub workspace_id: String,
|
||||
pub limit: usize,
|
||||
pub items: Vec<TicketSummary>,
|
||||
pub invalid_records: Vec<InvalidProjectRecord>,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketDetail {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
@@ -41,6 +54,7 @@ pub struct TicketDetail {
|
||||
pub updated_at: Option<String>,
|
||||
pub queued_by: Option<String>,
|
||||
pub queued_at: Option<String>,
|
||||
pub assignee: Option<String>,
|
||||
pub repository_id: Option<String>,
|
||||
pub ref_selector: Option<String>,
|
||||
pub risk_flags: Vec<String>,
|
||||
@@ -50,11 +64,13 @@ pub struct TicketDetail {
|
||||
pub events: Vec<TicketEventDetail>,
|
||||
pub artifact_count: usize,
|
||||
pub artifacts: Vec<String>,
|
||||
pub relations: TicketRelationView,
|
||||
pub resolution: Option<String>,
|
||||
pub record_source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketEventDetail {
|
||||
pub sequence: usize,
|
||||
pub kind: String,
|
||||
@@ -69,6 +85,106 @@ pub struct TicketEventDetail {
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelation {
|
||||
pub ticket_id: String,
|
||||
pub kind: String,
|
||||
pub target: String,
|
||||
pub note: Option<String>,
|
||||
pub author: String,
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct DerivedTicketRelation {
|
||||
pub source_ticket: String,
|
||||
pub inverse_kind: String,
|
||||
pub forward_kind: String,
|
||||
pub note: Option<String>,
|
||||
pub author: String,
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationBlocker {
|
||||
pub blocking_ticket: String,
|
||||
pub reason_kind: String,
|
||||
pub relation_kind: String,
|
||||
pub note: Option<String>,
|
||||
pub blocking_state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationNotice {
|
||||
pub related_ticket: String,
|
||||
pub kind: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationView {
|
||||
pub outgoing: Vec<TicketRelation>,
|
||||
pub incoming: Vec<DerivedTicketRelation>,
|
||||
pub blockers: Vec<TicketRelationBlocker>,
|
||||
pub notices: Vec<TicketRelationNotice>,
|
||||
}
|
||||
|
||||
impl From<ticket::TicketRelationView> for TicketRelationView {
|
||||
fn from(value: ticket::TicketRelationView) -> Self {
|
||||
Self {
|
||||
outgoing: value
|
||||
.outgoing
|
||||
.into_iter()
|
||||
.map(|relation| TicketRelation {
|
||||
ticket_id: relation.ticket_id,
|
||||
kind: relation.kind.as_str().to_string(),
|
||||
target: relation.target,
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
incoming: value
|
||||
.incoming
|
||||
.into_iter()
|
||||
.map(|relation| DerivedTicketRelation {
|
||||
source_ticket: relation.source_ticket,
|
||||
inverse_kind: relation.inverse_kind,
|
||||
forward_kind: relation.forward_kind.as_str().to_string(),
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
blockers: value
|
||||
.blockers
|
||||
.into_iter()
|
||||
.map(|blocker| TicketRelationBlocker {
|
||||
blocking_ticket: blocker.blocking_ticket,
|
||||
reason_kind: blocker.reason_kind,
|
||||
relation_kind: blocker.relation_kind.as_str().to_string(),
|
||||
note: blocker.note,
|
||||
blocking_state: blocker.blocking_state.as_str().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
notices: value
|
||||
.notices
|
||||
.into_iter()
|
||||
.map(|notice| TicketRelationNotice {
|
||||
related_ticket: notice.related_ticket,
|
||||
kind: notice.kind.as_str().to_string(),
|
||||
message: notice.message,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ObjectiveSummary {
|
||||
pub id: String,
|
||||
@@ -102,6 +218,62 @@ pub struct ObjectiveResourceSummary {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "typescript")]
|
||||
pub fn ticket_api_typescript() -> String {
|
||||
use ts_rs::TS;
|
||||
|
||||
let config = ts_rs::Config::default();
|
||||
let declarations = [
|
||||
InvalidProjectRecord::decl(&config),
|
||||
TicketSummary::decl(&config),
|
||||
TicketListResponse::decl(&config),
|
||||
TicketEventDetail::decl(&config),
|
||||
TicketRelation::decl(&config),
|
||||
DerivedTicketRelation::decl(&config),
|
||||
TicketRelationBlocker::decl(&config),
|
||||
TicketRelationNotice::decl(&config),
|
||||
TicketRelationView::decl(&config),
|
||||
TicketDetail::decl(&config),
|
||||
];
|
||||
format!(
|
||||
"// Generated from yoi-workspace-server. Do not edit by hand.\n// Regenerate: cargo run -q -p yoi-workspace-server --features typescript --example generate_ticket_api_types > web/workspace/src/lib/generated/ticket-api.ts\n\n{}\n",
|
||||
declarations
|
||||
.into_iter()
|
||||
.map(|declaration| format!("export {declaration}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n")
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "typescript"))]
|
||||
mod typescript_tests {
|
||||
#[test]
|
||||
fn generated_ticket_api_contract_is_current() {
|
||||
let expected = super::ticket_api_typescript();
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../web/workspace/src/lib/generated/ticket-api.ts");
|
||||
let actual = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
|
||||
assert_eq!(
|
||||
normalize(&actual),
|
||||
normalize(&expected),
|
||||
"regenerate Ticket API TypeScript types with `cargo run -q -p yoi-workspace-server --features typescript --example generate_ticket_api_types > web/workspace/src/lib/generated/ticket-api.ts` and format the generated file",
|
||||
);
|
||||
}
|
||||
|
||||
fn normalize(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter_map(|character| match character {
|
||||
character if character.is_whitespace() => None,
|
||||
',' => Some(';'),
|
||||
character => Some(character),
|
||||
})
|
||||
.collect::<String>()
|
||||
.replace(";}", "}")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_project_id(id: &str) -> Result<()> {
|
||||
validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string()))
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ use crate::profile_settings::{
|
||||
UpdateWorkspaceMetadataRequest, UpdateWorkspaceProfileRegistryRequest,
|
||||
UpdateWorkspaceProfileSourceRequest, WriteWorkspaceProfileTreeFileRequest,
|
||||
};
|
||||
use crate::records::{ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary};
|
||||
use crate::records::{ObjectiveDetail, ProjectRecordList, TicketDetail};
|
||||
use crate::repositories::{
|
||||
ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError,
|
||||
RepositoryRegistryReader, RepositorySummary,
|
||||
@@ -577,14 +577,6 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
||||
"/api/w/{workspace_id}/repositories/{repository_id}/log",
|
||||
get(scoped_repository_log),
|
||||
)
|
||||
.route(
|
||||
"/api/repositories/{repository_id}/tickets",
|
||||
get(repository_tickets),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/repositories/{repository_id}/tickets",
|
||||
get(scoped_repository_tickets),
|
||||
)
|
||||
.route("/api/hosts", get(list_hosts))
|
||||
.route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts))
|
||||
.route(
|
||||
@@ -1198,34 +1190,11 @@ pub struct RepositoryLogResponse {
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RepositoryTicketsResponse {
|
||||
pub workspace_id: String,
|
||||
pub repository_id: String,
|
||||
pub limit: usize,
|
||||
pub columns: Vec<TicketKanbanColumn>,
|
||||
pub invalid_records: Vec<crate::records::InvalidProjectRecord>,
|
||||
pub record_authority: String,
|
||||
pub source: String,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TicketKanbanColumn {
|
||||
pub state: String,
|
||||
pub items: Vec<TicketSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LogQuery {
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TicketKanbanQuery {
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MemoryStagingQuery {
|
||||
limit: Option<usize>,
|
||||
@@ -1256,6 +1225,11 @@ struct ObjectiveEditRequest {
|
||||
replace_all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TicketListQuery {
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ObjectiveStateRequest {
|
||||
state: String,
|
||||
@@ -1540,8 +1514,8 @@ async fn scoped_delete_profile_source(
|
||||
async fn scoped_list_tickets(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
Query(query): Query<TicketKanbanQuery>,
|
||||
) -> ApiResult<Json<ListResponse<crate::records::TicketSummary>>> {
|
||||
Query(query): Query<TicketListQuery>,
|
||||
) -> ApiResult<Json<crate::records::TicketListResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
list_tickets(State(api), Query(query)).await
|
||||
}
|
||||
@@ -2232,15 +2206,6 @@ async fn scoped_repository_log(
|
||||
repository_log(State(api), AxumPath(path.repository_id), Query(query)).await
|
||||
}
|
||||
|
||||
async fn scoped_repository_tickets(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRepositoryPath>,
|
||||
Query(query): Query<TicketKanbanQuery>,
|
||||
) -> ApiResult<Json<RepositoryTicketsResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
repository_tickets(State(api), AxumPath(path.repository_id), Query(query)).await
|
||||
}
|
||||
|
||||
async fn scoped_list_hosts(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
@@ -4111,8 +4076,8 @@ fn companion_console_extension_point(status: &CompanionStatusResponse) -> Extens
|
||||
|
||||
async fn list_tickets(
|
||||
State(api): State<WorkspaceApi>,
|
||||
Query(query): Query<TicketKanbanQuery>,
|
||||
) -> ApiResult<Json<ListResponse<crate::records::TicketSummary>>> {
|
||||
Query(query): Query<TicketListQuery>,
|
||||
) -> ApiResult<Json<crate::records::TicketListResponse>> {
|
||||
let requested_limit = query.limit.unwrap_or(api.config.max_records);
|
||||
let limit = requested_limit.min(1000);
|
||||
let ProjectRecordList {
|
||||
@@ -4120,7 +4085,7 @@ async fn list_tickets(
|
||||
invalid_records,
|
||||
record_authority,
|
||||
} = api.authority.list_tickets(limit)?;
|
||||
Ok(Json(ListResponse {
|
||||
Ok(Json(crate::records::TicketListResponse {
|
||||
workspace_id: api.config.workspace_id,
|
||||
limit,
|
||||
items,
|
||||
@@ -4211,35 +4176,6 @@ async fn repository_log(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn repository_tickets(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(repository_id): AxumPath<String>,
|
||||
Query(query): Query<TicketKanbanQuery>,
|
||||
) -> ApiResult<Json<RepositoryTicketsResponse>> {
|
||||
repository_lookup(api.repository_reader().summary(&repository_id))?;
|
||||
let canonical_repository_id = repository_id;
|
||||
let limit = query.limit.unwrap_or(api.config.max_records).min(200);
|
||||
let ProjectRecordList {
|
||||
items,
|
||||
invalid_records,
|
||||
record_authority,
|
||||
} = api.authority.list_tickets(limit)?;
|
||||
Ok(Json(RepositoryTicketsResponse {
|
||||
workspace_id: api.config.workspace_id,
|
||||
repository_id: canonical_repository_id,
|
||||
limit,
|
||||
columns: ticket_kanban_columns(items),
|
||||
invalid_records,
|
||||
record_authority,
|
||||
source: "workspace_local_ticket_fallback".to_string(),
|
||||
diagnostics: vec![RuntimeDiagnostic {
|
||||
code: "repository_ticket_target_metadata_absent".to_string(),
|
||||
severity: DiagnosticSeverity::Info,
|
||||
message: "Ticket target Repository metadata is not available yet; Kanban groups all workspace-local Tickets by state as a read-only fallback.".to_string(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
|
||||
async fn list_hosts(
|
||||
State(api): State<WorkspaceApi>,
|
||||
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
|
||||
@@ -6811,52 +6747,6 @@ fn repository_lookup<T>(result: std::result::Result<T, RepositoryLookupError>) -
|
||||
})
|
||||
}
|
||||
|
||||
fn ticket_kanban_columns(items: Vec<TicketSummary>) -> Vec<TicketKanbanColumn> {
|
||||
let mut columns = vec![
|
||||
TicketKanbanColumn {
|
||||
state: "planning".to_string(),
|
||||
items: Vec::new(),
|
||||
},
|
||||
TicketKanbanColumn {
|
||||
state: "ready".to_string(),
|
||||
items: Vec::new(),
|
||||
},
|
||||
TicketKanbanColumn {
|
||||
state: "queued".to_string(),
|
||||
items: Vec::new(),
|
||||
},
|
||||
TicketKanbanColumn {
|
||||
state: "inprogress".to_string(),
|
||||
items: Vec::new(),
|
||||
},
|
||||
TicketKanbanColumn {
|
||||
state: "done".to_string(),
|
||||
items: Vec::new(),
|
||||
},
|
||||
TicketKanbanColumn {
|
||||
state: "closed".to_string(),
|
||||
items: Vec::new(),
|
||||
},
|
||||
TicketKanbanColumn {
|
||||
state: "other".to_string(),
|
||||
items: Vec::new(),
|
||||
},
|
||||
];
|
||||
for item in items {
|
||||
let index = match item.state.as_str() {
|
||||
"planning" => 0,
|
||||
"ready" => 1,
|
||||
"queued" => 2,
|
||||
"inprogress" => 3,
|
||||
"done" => 4,
|
||||
"closed" => 5,
|
||||
_ => 6,
|
||||
};
|
||||
columns[index].items.push(item);
|
||||
}
|
||||
columns
|
||||
}
|
||||
|
||||
async fn static_or_spa_fallback(State(api): State<WorkspaceApi>, uri: Uri) -> Response {
|
||||
if uri.path().starts_with("/api/") || uri.path() == "/api" {
|
||||
return (
|
||||
@@ -7990,6 +7880,40 @@ mod tests {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
id: ticket_id.clone(),
|
||||
};
|
||||
let Json(related) = scoped_ticket_backend_operation(
|
||||
State(api.clone()),
|
||||
AxumPath(ScopedWorkspacePath {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
}),
|
||||
Json(TicketBackendOperation::Create {
|
||||
input: ticket::NewTicket::new("Related Browser Ticket"),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let related_ticket_id = match related {
|
||||
TicketBackendHttpResponse::Ok {
|
||||
result: ticket::TicketBackendOperationResult::TicketRef(ticket_ref),
|
||||
} => ticket_ref.id,
|
||||
other => panic!("unexpected related create response: {other:?}"),
|
||||
};
|
||||
let _ = scoped_ticket_backend_operation(
|
||||
State(api.clone()),
|
||||
AxumPath(ScopedWorkspacePath {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
}),
|
||||
Json(TicketBackendOperation::AddTicketRelation {
|
||||
id: ticket_id.clone().into(),
|
||||
relation: ticket::NewTicketRelation {
|
||||
kind: ticket::TicketRelationKind::Related,
|
||||
target: related_ticket_id.clone(),
|
||||
note: Some("Browser relation".to_string()),
|
||||
author: Some("browser-user".to_string()),
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let Json(edited) = scoped_edit_ticket_item(
|
||||
State(api.clone()),
|
||||
@@ -8013,6 +7937,10 @@ mod tests {
|
||||
assert_eq!(edited.body, "Updated from the Browser API.");
|
||||
assert_eq!(edited.repository_id.as_deref(), Some("main"));
|
||||
assert_eq!(edited.ref_selector.as_deref(), Some("feature/api"));
|
||||
assert_eq!(edited.assignee, None);
|
||||
assert_eq!(edited.relations.outgoing.len(), 1);
|
||||
assert_eq!(edited.relations.outgoing[0].target, related_ticket_id);
|
||||
assert_eq!(edited.relations.outgoing[0].kind, "related");
|
||||
|
||||
let Json(commented) = scoped_append_ticket_event(
|
||||
State(api.clone()),
|
||||
@@ -9408,19 +9336,17 @@ mod tests {
|
||||
assert_eq!(repository_log["default_selector"], "HEAD");
|
||||
assert_eq!(repository_log["limit"], 3);
|
||||
|
||||
let repository_tickets = get_json(app.clone(), "/api/repositories/main/tickets").await;
|
||||
assert_eq!(repository_tickets["repository_id"], TEST_REPOSITORY_ID);
|
||||
let ready_column = repository_tickets["columns"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|column| column["state"] == "ready")
|
||||
let removed_repository_tickets = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/repositories/main/tickets")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(ready_column["items"][0]["title"], "API Ticket");
|
||||
assert_eq!(
|
||||
repository_tickets["diagnostics"][0]["code"],
|
||||
"repository_ticket_target_metadata_absent"
|
||||
);
|
||||
assert_eq!(removed_repository_tickets.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let unknown_repository_response = app
|
||||
.clone()
|
||||
|
||||
Reference in New Issue
Block a user