fix: restore workspace routing contracts
This commit is contained in:
@@ -923,6 +923,7 @@ fn is_server_global_forward(path: &str) -> bool {
|
||||
|| path.starts_with("/api/auth/")
|
||||
|| path == "/health"
|
||||
|| path == "/"
|
||||
|| path.starts_with("/_app/")
|
||||
|| path.starts_with("/assets/")
|
||||
}
|
||||
|
||||
@@ -12502,7 +12503,7 @@ async fn static_or_spa_fallback(State(api): State<WorkspaceApi>, uri: Uri) -> Re
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
};
|
||||
|
||||
match read_static_or_index(static_root, uri.path()).await {
|
||||
match read_static_or_index(static_root, scoped_workspace_static_path(uri.path())).await {
|
||||
Ok(StaticAsset {
|
||||
bytes,
|
||||
content_type,
|
||||
@@ -12553,6 +12554,16 @@ struct StaticAsset {
|
||||
content_type: &'static str,
|
||||
}
|
||||
|
||||
fn scoped_workspace_static_path(path: &str) -> &str {
|
||||
let Some(scoped) = path.strip_prefix("/w/") else {
|
||||
return path;
|
||||
};
|
||||
match scoped.find('/') {
|
||||
Some(index) => &scoped[index..],
|
||||
None => "/",
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_static_or_index(root: &Path, request_path: &str) -> Result<StaticAsset> {
|
||||
let candidate = safe_static_candidate(root, request_path)?;
|
||||
let file = if tokio::fs::metadata(&candidate)
|
||||
@@ -14427,7 +14438,20 @@ mod tests {
|
||||
let repository_b = dir.path().join("repository-b");
|
||||
std::fs::create_dir_all(repository_a.join(".git")).unwrap();
|
||||
std::fs::create_dir_all(repository_b.join(".git")).unwrap();
|
||||
let template = test_server_config(dir.path());
|
||||
let static_dir = dir.path().join("static");
|
||||
std::fs::create_dir_all(static_dir.join("_app/immutable/entry")).unwrap();
|
||||
std::fs::write(
|
||||
static_dir.join("index.html"),
|
||||
"<main>Workspace chooser</main>",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
static_dir.join("_app/immutable/entry/start.js"),
|
||||
"console.log('workspace app');",
|
||||
)
|
||||
.unwrap();
|
||||
let mut template = test_server_config(dir.path());
|
||||
template.static_assets_dir = Some(static_dir);
|
||||
let store = Arc::new(SqliteWorkspaceStore::open(&template.database_path).unwrap());
|
||||
let catalog = WorkspaceCatalogService::new(store.clone());
|
||||
let workspace_a = catalog
|
||||
@@ -14470,6 +14494,36 @@ mod tests {
|
||||
assert_eq!(b["workspace_id"], workspace_b.workspace.workspace_id);
|
||||
assert_eq!(b["display_name"], "Workspace B");
|
||||
|
||||
for asset_uri in [
|
||||
"/_app/immutable/entry/start.js".to_string(),
|
||||
format!(
|
||||
"/w/{}/_app/immutable/entry/start.js",
|
||||
workspace_a.workspace.workspace_id
|
||||
),
|
||||
] {
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(asset_uri)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.to_vec(),
|
||||
)
|
||||
.unwrap(),
|
||||
"console.log('workspace app');"
|
||||
);
|
||||
}
|
||||
|
||||
let handle = missing_resource_handle();
|
||||
let resource_response = app
|
||||
.clone()
|
||||
|
||||
@@ -14,7 +14,10 @@ export type WorkspaceProfileApi = {
|
||||
getProfiles(workspaceId: string): Promise<ProfileSettingsResponse>;
|
||||
};
|
||||
|
||||
async function requestJson<T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> {
|
||||
async function requestJson<T>(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
): Promise<T> {
|
||||
const response = await fetch(input, init);
|
||||
if (!response.ok) {
|
||||
throw new Error(`request failed: ${response.status}`);
|
||||
@@ -44,7 +47,9 @@ export async function updateWorkspaceMetadataSettings(
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchProfileSettings(workspaceId: string): Promise<ProfileSettingsResponse> {
|
||||
export async function fetchProfileSettings(
|
||||
workspaceId: string,
|
||||
): Promise<ProfileSettingsResponse> {
|
||||
return await requestJson<ProfileSettingsResponse>(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`,
|
||||
);
|
||||
@@ -52,20 +57,12 @@ export async function fetchProfileSettings(workspaceId: string): Promise<Profile
|
||||
|
||||
export function createWorkspaceProfileApi(): WorkspaceProfileApi {
|
||||
return {
|
||||
async getMetadata(workspaceId) {
|
||||
return await requestJson<WorkspaceMetadataSettingsResponse>(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/metadata`,
|
||||
);
|
||||
},
|
||||
getMetadata: fetchWorkspaceMetadataSettings,
|
||||
async updateMetadata(workspaceId, displayName, expectedRevision) {
|
||||
return await requestJson<WorkspaceMetadataMutationResponse>(
|
||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/metadata`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ display_name: displayName, expected_revision: expectedRevision }),
|
||||
},
|
||||
);
|
||||
return await updateWorkspaceMetadataSettings(workspaceId, {
|
||||
display_name: displayName,
|
||||
revision: expectedRevision,
|
||||
});
|
||||
},
|
||||
getProfiles: fetchProfileSettings,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => void | Promise<void>): void;
|
||||
};
|
||||
|
||||
import { createWorkspaceProfileApi } from "../src/lib/workspace/settings/profile-api.ts";
|
||||
|
||||
Deno.test("workspace profile API delegates metadata calls to current route contract", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests: Array<{ input: string; init?: RequestInit }> = [];
|
||||
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
requests.push({ input: String(input), init });
|
||||
return Promise.resolve(Response.json({
|
||||
workspace_id: "workspace-a",
|
||||
display_name: "Alpha",
|
||||
revision: "revision-2",
|
||||
}));
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const api = createWorkspaceProfileApi();
|
||||
await api.getMetadata("workspace-a");
|
||||
await api.updateMetadata("workspace-a", "Alpha updated", "revision-1");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
if (requests.length !== 2) throw new Error("expected two metadata requests");
|
||||
if (
|
||||
requests.some((request) => request.input.includes("/settings/metadata"))
|
||||
) {
|
||||
throw new Error("obsolete metadata endpoint was used");
|
||||
}
|
||||
if (
|
||||
requests.some((request) => !request.input.endsWith("/settings/workspace"))
|
||||
) {
|
||||
throw new Error("current Workspace settings endpoint was not used");
|
||||
}
|
||||
const updateBody = JSON.parse(String(requests[1].init?.body));
|
||||
if (
|
||||
updateBody.display_name !== "Alpha updated" ||
|
||||
updateBody.revision !== "revision-1" ||
|
||||
"expected_revision" in updateBody
|
||||
) {
|
||||
throw new Error(`unexpected update payload: ${JSON.stringify(updateBody)}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user