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.starts_with("/api/auth/")
|
||||||
|| path == "/health"
|
|| path == "/health"
|
||||||
|| path == "/"
|
|| path == "/"
|
||||||
|
|| path.starts_with("/_app/")
|
||||||
|| path.starts_with("/assets/")
|
|| 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();
|
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 {
|
Ok(StaticAsset {
|
||||||
bytes,
|
bytes,
|
||||||
content_type,
|
content_type,
|
||||||
@@ -12553,6 +12554,16 @@ struct StaticAsset {
|
|||||||
content_type: &'static str,
|
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> {
|
async fn read_static_or_index(root: &Path, request_path: &str) -> Result<StaticAsset> {
|
||||||
let candidate = safe_static_candidate(root, request_path)?;
|
let candidate = safe_static_candidate(root, request_path)?;
|
||||||
let file = if tokio::fs::metadata(&candidate)
|
let file = if tokio::fs::metadata(&candidate)
|
||||||
@@ -14427,7 +14438,20 @@ mod tests {
|
|||||||
let repository_b = dir.path().join("repository-b");
|
let repository_b = dir.path().join("repository-b");
|
||||||
std::fs::create_dir_all(repository_a.join(".git")).unwrap();
|
std::fs::create_dir_all(repository_a.join(".git")).unwrap();
|
||||||
std::fs::create_dir_all(repository_b.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 store = Arc::new(SqliteWorkspaceStore::open(&template.database_path).unwrap());
|
||||||
let catalog = WorkspaceCatalogService::new(store.clone());
|
let catalog = WorkspaceCatalogService::new(store.clone());
|
||||||
let workspace_a = catalog
|
let workspace_a = catalog
|
||||||
@@ -14470,6 +14494,36 @@ mod tests {
|
|||||||
assert_eq!(b["workspace_id"], workspace_b.workspace.workspace_id);
|
assert_eq!(b["workspace_id"], workspace_b.workspace.workspace_id);
|
||||||
assert_eq!(b["display_name"], "Workspace B");
|
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 handle = missing_resource_handle();
|
||||||
let resource_response = app
|
let resource_response = app
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export type WorkspaceProfileApi = {
|
|||||||
getProfiles(workspaceId: string): Promise<ProfileSettingsResponse>;
|
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);
|
const response = await fetch(input, init);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`request failed: ${response.status}`);
|
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>(
|
return await requestJson<ProfileSettingsResponse>(
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`,
|
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`,
|
||||||
);
|
);
|
||||||
@@ -52,20 +57,12 @@ export async function fetchProfileSettings(workspaceId: string): Promise<Profile
|
|||||||
|
|
||||||
export function createWorkspaceProfileApi(): WorkspaceProfileApi {
|
export function createWorkspaceProfileApi(): WorkspaceProfileApi {
|
||||||
return {
|
return {
|
||||||
async getMetadata(workspaceId) {
|
getMetadata: fetchWorkspaceMetadataSettings,
|
||||||
return await requestJson<WorkspaceMetadataSettingsResponse>(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/metadata`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async updateMetadata(workspaceId, displayName, expectedRevision) {
|
async updateMetadata(workspaceId, displayName, expectedRevision) {
|
||||||
return await requestJson<WorkspaceMetadataMutationResponse>(
|
return await updateWorkspaceMetadataSettings(workspaceId, {
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/metadata`,
|
display_name: displayName,
|
||||||
{
|
revision: expectedRevision,
|
||||||
method: "PUT",
|
});
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
body: JSON.stringify({ display_name: displayName, expected_revision: expectedRevision }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
getProfiles: fetchProfileSettings,
|
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