feat: scope workspace routes by id

This commit is contained in:
2026-07-07 05:26:03 +09:00
parent 34836d97ae
commit f6ad9cfcd3
36 changed files with 2082 additions and 89 deletions
+2
View File
@@ -71,6 +71,8 @@ pub enum Error {
},
#[error("unknown local repository `{0}`")]
UnknownRepository(String),
#[error("workspace id does not match this Workspace backend")]
WorkspaceIdMismatch,
#[error("workspace identity error: {0}")]
WorkspaceIdentity(String),
#[error("store error: {0}")]
+543 -3
View File
@@ -221,84 +221,193 @@ impl WorkspaceApi {
pub fn build_router(api: WorkspaceApi) -> Router {
Router::new()
.route("/api/workspace", get(get_workspace))
.route("/api/w/{workspace_id}/workspace", get(scoped_get_workspace))
.route("/api/tickets", get(list_tickets))
.route("/api/w/{workspace_id}/tickets", get(scoped_list_tickets))
.route("/api/tickets/{id}", get(get_ticket))
.route("/api/w/{workspace_id}/tickets/{id}", get(scoped_get_ticket))
.route("/api/objectives", get(list_objectives))
.route(
"/api/w/{workspace_id}/objectives",
get(scoped_list_objectives),
)
.route("/api/objectives/{id}", get(get_objective))
.route(
"/api/w/{workspace_id}/objectives/{id}",
get(scoped_get_objective),
)
.route("/api/repositories", get(list_repositories))
.route(
"/api/w/{workspace_id}/repositories",
get(scoped_list_repositories),
)
.route("/api/repositories/{repository_id}", get(repository_detail))
.route(
"/api/w/{workspace_id}/repositories/{repository_id}",
get(scoped_repository_detail),
)
.route("/api/repositories/{repository_id}/log", get(repository_log))
.route(
"/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("/api/runtimes", get(list_runtimes))
.route("/api/w/{workspace_id}/runtimes", get(scoped_list_runtimes))
.route(
"/api/workers",
get(list_workers).post(create_workspace_worker),
)
.route(
"/api/w/{workspace_id}/workers",
get(scoped_list_workers).post(scoped_create_workspace_worker),
)
.route(
"/api/workers/launch-options",
get(get_worker_launch_options),
)
.route(
"/api/w/{workspace_id}/workers/launch-options",
get(scoped_get_worker_launch_options),
)
.route(
"/api/settings/runtime-connections",
get(get_runtime_connection_settings),
)
.route(
"/api/w/{workspace_id}/settings/runtime-connections",
get(scoped_get_runtime_connection_settings),
)
.route(
"/api/settings/runtime-connections/remotes",
post(add_remote_runtime_connection),
)
.route(
"/api/w/{workspace_id}/settings/runtime-connections/remotes",
post(scoped_add_remote_runtime_connection),
)
.route(
"/api/settings/runtime-connections/remotes/{runtime_id}",
delete(delete_remote_runtime_connection),
)
.route(
"/api/w/{workspace_id}/settings/runtime-connections/remotes/{runtime_id}",
delete(scoped_delete_remote_runtime_connection),
)
.route(
"/api/settings/runtime-connections/remotes/{runtime_id}/test",
post(test_remote_runtime_connection),
)
.route(
"/api/w/{workspace_id}/settings/runtime-connections/remotes/{runtime_id}/test",
post(scoped_test_remote_runtime_connection),
)
.route("/api/companion/status", get(get_companion_status))
.route(
"/api/w/{workspace_id}/companion/status",
get(scoped_get_companion_status),
)
.route("/api/companion/transcript", get(get_companion_transcript))
.route(
"/api/w/{workspace_id}/companion/transcript",
get(scoped_get_companion_transcript),
)
.route("/api/companion/messages", post(post_companion_message))
.route(
"/api/w/{workspace_id}/companion/messages",
post(scoped_post_companion_message),
)
.route("/api/companion/cancel", post(post_companion_cancel))
.route(
"/api/w/{workspace_id}/companion/cancel",
post(scoped_post_companion_cancel),
)
.route(
"/api/runtimes/{runtime_id}/workers",
post(create_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers",
post(scoped_create_runtime_worker),
)
.route(
"/api/runtimes/{runtime_id}/config-bundles",
post(sync_runtime_config_bundle),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/config-bundles",
post(scoped_sync_runtime_config_bundle),
)
.route(
"/api/runtimes/{runtime_id}/config-bundles/{bundle_id}/availability",
get(check_runtime_config_bundle),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/config-bundles/{bundle_id}/availability",
get(scoped_check_runtime_config_bundle),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}",
get(get_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}",
get(scoped_get_runtime_worker),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/input",
post(send_runtime_worker_input),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/input",
post(scoped_send_runtime_worker_input),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/stop",
post(stop_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/stop",
post(scoped_stop_runtime_worker),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/cancel",
post(cancel_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/cancel",
post(scoped_cancel_runtime_worker),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/transcript",
get(get_runtime_worker_transcript),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/transcript",
get(scoped_get_runtime_worker_transcript),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/events/ws",
get(worker_observation_ws),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/events/ws",
get(scoped_worker_observation_ws),
)
.route("/api/hosts/{host_id}/workers", get(list_host_workers))
.route(
"/api/w/{workspace_id}/hosts/{host_id}/workers",
get(scoped_list_host_workers),
)
.fallback(get(static_or_spa_fallback))
.with_state(api)
}
@@ -522,6 +631,363 @@ struct TranscriptQuery {
limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct ScopedWorkspacePath {
workspace_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRecordPath {
workspace_id: String,
id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRepositoryPath {
workspace_id: String,
repository_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedHostPath {
workspace_id: String,
host_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRuntimePath {
workspace_id: String,
runtime_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedConfigBundlePath {
workspace_id: String,
runtime_id: String,
bundle_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRuntimeWorkerPath {
workspace_id: String,
runtime_id: String,
worker_id: String,
}
fn validate_workspace_scope(api: &WorkspaceApi, workspace_id: &str) -> ApiResult<()> {
if workspace_id == api.workspace_id() {
Ok(())
} else {
Err(workspace_id_mismatch_error())
}
}
async fn scoped_get_workspace(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<WorkspaceResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_workspace(State(api)).await
}
async fn scoped_list_tickets(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<ListResponse<crate::records::TicketSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_tickets(State(api)).await
}
async fn scoped_get_ticket(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_ticket(State(api), AxumPath(path.id)).await
}
async fn scoped_list_objectives(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<ListResponse<crate::records::ObjectiveSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_objectives(State(api)).await
}
async fn scoped_get_objective(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
) -> ApiResult<Json<ObjectiveDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_objective(State(api), AxumPath(path.id)).await
}
async fn scoped_list_repositories(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RepositoryListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_repositories(State(api)).await
}
async fn scoped_repository_detail(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRepositoryPath>,
) -> ApiResult<Json<RepositoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
repository_detail(State(api), AxumPath(path.repository_id)).await
}
async fn scoped_repository_log(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRepositoryPath>,
Query(query): Query<LogQuery>,
) -> ApiResult<Json<RepositoryLogResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
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>,
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_hosts(State(api)).await
}
async fn scoped_list_runtimes(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeListResponse<RuntimeSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_runtimes(State(api)).await
}
async fn scoped_list_workers(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_workers(State(api)).await
}
async fn scoped_create_workspace_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<BrowserCreateWorkerRequest>,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
create_workspace_worker(State(api), Json(request)).await
}
async fn scoped_get_worker_launch_options(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<WorkerLaunchOptionsResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_worker_launch_options(State(api)).await
}
async fn scoped_get_runtime_connection_settings(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeConnectionSettingsResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_runtime_connection_settings(State(api)).await
}
async fn scoped_add_remote_runtime_connection(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<AddRemoteRuntimeConnectionRequest>,
) -> ApiResult<Json<RuntimeConnectionMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
add_remote_runtime_connection(State(api), Json(request)).await
}
async fn scoped_delete_remote_runtime_connection(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
) -> ApiResult<Json<RuntimeConnectionMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
delete_remote_runtime_connection(State(api), AxumPath(path.runtime_id)).await
}
async fn scoped_test_remote_runtime_connection(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
) -> ApiResult<Json<RemoteRuntimeTestResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
test_remote_runtime_connection(State(api), AxumPath(path.runtime_id)).await
}
async fn scoped_get_companion_status(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<CompanionStatusResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_companion_status(State(api)).await
}
async fn scoped_get_companion_transcript(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Query(query): Query<TranscriptQuery>,
) -> ApiResult<Json<CompanionTranscriptProjection>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_companion_transcript(State(api), Query(query)).await
}
async fn scoped_post_companion_message(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<CompanionMessageRequest>,
) -> ApiResult<Json<CompanionMessageResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
post_companion_message(State(api), Json(request)).await
}
async fn scoped_post_companion_cancel(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<CompanionCancelRequest>,
) -> ApiResult<Json<CompanionMessageResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
post_companion_cancel(State(api), Json(request)).await
}
async fn scoped_create_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Json(request): Json<WorkerSpawnRequest>,
) -> ApiResult<Json<WorkerSpawnResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
create_runtime_worker(State(api), AxumPath(path.runtime_id), Json(request)).await
}
async fn scoped_sync_runtime_config_bundle(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Json(request): Json<RuntimeConfigBundleSyncRequest>,
) -> ApiResult<Json<ConfigBundleSyncResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
sync_runtime_config_bundle(State(api), AxumPath(path.runtime_id), Json(request)).await
}
async fn scoped_check_runtime_config_bundle(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedConfigBundlePath>,
Query(query): Query<RuntimeConfigBundleAvailabilityQuery>,
) -> ApiResult<Json<ConfigBundleCheckResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
check_runtime_config_bundle(
State(api),
AxumPath((path.runtime_id, path.bundle_id)),
Query(query),
)
.await
}
async fn scoped_get_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
) -> ApiResult<Json<WorkerSummary>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_runtime_worker(State(api), AxumPath((path.runtime_id, path.worker_id))).await
}
async fn scoped_send_runtime_worker_input(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Json(request): Json<WorkerInputRequest>,
) -> ApiResult<Json<WorkerInputResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
send_runtime_worker_input(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Json(request),
)
.await
}
async fn scoped_stop_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Json(request): Json<WorkerLifecycleRequest>,
) -> ApiResult<Json<WorkerLifecycleResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
stop_runtime_worker(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Json(request),
)
.await
}
async fn scoped_cancel_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Json(request): Json<WorkerLifecycleRequest>,
) -> ApiResult<Json<WorkerLifecycleResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
cancel_runtime_worker(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Json(request),
)
.await
}
async fn scoped_get_runtime_worker_transcript(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Query(query): Query<TranscriptQuery>,
) -> ApiResult<Json<WorkerTranscriptProjection>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_runtime_worker_transcript(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Query(query),
)
.await
}
async fn scoped_worker_observation_ws(
ws: WebSocketUpgrade,
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Query(query): Query<ClientWorkerEventsWsQuery>,
) -> Response {
if let Err(err) = validate_workspace_scope(&api, &path.workspace_id) {
return err.into_response();
}
worker_observation_ws(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Query(query),
ws,
)
.await
.into_response()
}
async fn scoped_list_host_workers(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedHostPath>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_host_workers(State(api), AxumPath(path.host_id)).await
}
async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<WorkspaceResponse>> {
let schema_version = api.store.schema_version().await?;
let stored = api.store.get_workspace(api.workspace_id()).await?;
@@ -1048,13 +1514,15 @@ async fn create_workspace_worker(
})?;
let runtime_id = worker.runtime_id.clone();
let worker_id = worker.worker_id.clone();
let workspace_id = api.workspace_id().to_string();
let console_href = format!(
"/runtimes/{}/workers/{}/console",
"/w/{}/runtimes/{}/workers/{}/console",
encode_path_segment(&workspace_id),
encode_path_segment(&runtime_id),
encode_path_segment(&worker_id)
);
Ok(Json(BrowserCreateWorkerResponse {
workspace_id: api.config.workspace_id,
workspace_id,
runtime_id,
worker_id,
console_href,
@@ -2263,6 +2731,12 @@ async fn static_or_spa_fallback(State(api): State<WorkspaceApi>, uri: Uri) -> Re
.into_response();
}
if let Some(workspace_id) = workspace_id_from_ui_path(uri.path()) {
if workspace_id != api.workspace_id() {
return workspace_id_mismatch_error().into_response();
}
}
let Some(static_root) = api.config.static_assets_dir.as_ref() else {
return StatusCode::NOT_FOUND.into_response();
};
@@ -2279,6 +2753,16 @@ async fn static_or_spa_fallback(State(api): State<WorkspaceApi>, uri: Uri) -> Re
}
}
fn workspace_id_from_ui_path(path: &str) -> Option<&str> {
let tail = path.strip_prefix("/w/")?;
let workspace_id = tail.split('/').next().unwrap_or_default();
if workspace_id.is_empty() {
None
} else {
Some(workspace_id)
}
}
struct StaticAsset {
bytes: Vec<u8>,
content_type: &'static str,
@@ -2337,6 +2821,18 @@ fn content_type_for(path: &Path) -> &'static str {
type ApiResult<T> = std::result::Result<T, ApiError>;
fn workspace_id_mismatch_error() -> ApiError {
let message = "workspace id does not match this Workspace backend".to_string();
ApiError::with_diagnostics(
Error::WorkspaceIdMismatch,
vec![RuntimeDiagnostic {
code: "workspace_id_mismatch".to_string(),
severity: DiagnosticSeverity::Error,
message,
}],
)
}
struct ApiError {
error: Error,
diagnostics: Vec<RuntimeDiagnostic>,
@@ -2366,7 +2862,8 @@ impl IntoResponse for ApiError {
| Error::UnknownHost(_)
| Error::UnknownRuntime(_)
| Error::UnknownWorker { .. }
| Error::UnknownRepository(_) => StatusCode::NOT_FOUND,
| Error::UnknownRepository(_)
| Error::WorkspaceIdMismatch => StatusCode::NOT_FOUND,
Error::Ticket(_) => StatusCode::NOT_FOUND,
Error::RuntimeCapabilityUnsupported { .. } => StatusCode::NOT_IMPLEMENTED,
Error::RuntimeOperationFailed { code, .. } if code == "repository_not_configured" => {
@@ -3076,6 +3573,31 @@ mod tests {
);
}
let scoped_workspace = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/workspace"),
)
.await;
assert_eq!(scoped_workspace["workspace_id"], TEST_WORKSPACE_ID);
let mismatched_workspace = request_json(
app.clone(),
"GET",
"/api/w/not-this-workspace/workspace",
None,
StatusCode::NOT_FOUND,
)
.await;
assert_eq!(
mismatched_workspace["diagnostics"][0]["code"],
"workspace_id_mismatch"
);
assert!(
!mismatched_workspace
.to_string()
.contains(dir.path().to_string_lossy().as_ref())
);
let tickets = get_json(app.clone(), "/api/tickets").await;
assert_eq!(tickets["items"][0]["id"], "00000000001J2");
assert_eq!(tickets["items"][0]["state"], "ready");
@@ -3083,6 +3605,18 @@ mod tests {
let objectives = get_json(app.clone(), "/api/objectives").await;
assert_eq!(objectives["items"][0]["id"], "00000000001J3");
assert_eq!(objectives["items"][0]["summary"], "Objective body.");
let scoped_objectives = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives"),
)
.await;
assert_eq!(scoped_objectives["items"][0]["id"], "00000000001J3");
let scoped_objective = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives/00000000001J3"),
)
.await;
assert_eq!(scoped_objective["id"], "00000000001J3");
let repositories = get_json(app.clone(), "/api/repositories").await;
assert_eq!(repositories["items"][0]["id"], TEST_REPOSITORY_ID);
@@ -3099,6 +3633,12 @@ mod tests {
let repository_detail = get_json(app.clone(), "/api/repositories/main").await;
assert_eq!(repository_detail["item"]["id"], TEST_REPOSITORY_ID);
let scoped_repository_detail = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/repositories/main"),
)
.await;
assert_eq!(scoped_repository_detail["item"]["id"], TEST_REPOSITORY_ID);
let repository_log = get_json(app.clone(), "/api/repositories/main/log?limit=3").await;
assert_eq!(repository_log["repository_id"], TEST_REPOSITORY_ID);