chore: Fix/build id sync from main (#5843)

This commit is contained in:
Oleg Isonen authored and GitHub committed 2026-07-01 15:57:26 +02:00
1 parent a7f1bc6e61
commit bfcdef65de
10 files changed
+192 -6

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
[submodule "packages/sdk-components-animation/private-src"]
path = packages/sdk-components-animation/private-src
url = https://github.com/webstudio-is/sdk-components-animation.git
url = git@github.com:webstudio-is/sdk-components-animation.git
branch = main
@@ -0,0 +1,76 @@
import { describe, expect, test } from "vitest";
import type { DashboardProject } from "@webstudio-is/dashboard";
import { searchProjects } from "./search-results";
const createProject = (
overrides: Partial<DashboardProject>
): DashboardProject =>
({
id: "project-1",
createdAt: "2024-01-01T00:00:00.000Z",
title: "Default Site",
domain: "default",
userId: "user-1",
isDeleted: false,
isPublished: false,
latestBuild: null,
previewImageAsset: null,
previewImageAssetId: null,
latestBuildVirtual: null,
marketplaceApprovalStatus: "UNLISTED",
tags: [],
domainsVirtual: [],
workspaceId: null,
...overrides,
}) as DashboardProject;
describe("searchProjects", () => {
test.each([
["id", "83e97c09dcce", { id: "d845c167-ea07-4875-b08d-83e97c09dcce" }],
["title", "Marketing", { title: "Marketing Site" }],
["domain", "docs", { domain: "docs" }],
[
"custom domain",
"client.example.com",
{
domainsVirtual: [
{ domain: "client.example.com", status: "ACTIVE", verified: true },
],
},
],
[
"latest build id",
"aca9e9574587",
{
latestBuildVirtual: {
buildId: "f565d527-32e7-4731-bc71-aca9e9574587",
projectId: "project-1",
domainsVirtualId: "",
domain: "fixture",
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
publishStatus: "PUBLISHED",
},
},
],
] satisfies Array<[string, string, Partial<DashboardProject>]>)(
"matches projects by %s",
(_field, search, overrides) => {
const projects = [
createProject(overrides),
createProject({ id: "project-other", title: "Other Project" }),
];
expect(searchProjects(projects, search)).toEqual([projects[0]]);
}
);
test("does not return projects without matching fields", () => {
const projects = [
createProject({ title: "Marketing Site" }),
createProject({ title: "Other Project" }),
];
expect(searchProjects(projects, "missing")).toEqual([]);
});
});
@@ -16,6 +16,22 @@ const initialSearchResults: SearchResults = {
projects: [],
} as const;
const searchProjectKeys = [
"id",
"title",
"domain",
(project: DashboardProject) =>
project.domainsVirtual.map(({ domain }) => domain),
(project: DashboardProject) => project.latestBuildVirtual?.buildId ?? "",
];
export const searchProjects = (
projects: Array<DashboardProject>,
search: string
) => {
return matchSorter(projects, search, { keys: searchProjectKeys });
};
export const SearchResults = (props: DashboardData) => {
const [searchParams] = useSearchParams();
const { projects, publisherHost } = props;
@@ -25,9 +41,8 @@ export const SearchResults = (props: DashboardData) => {
if (!search || !projects) {
return initialSearchResults;
}
const keys = ["title", "domain"];
return {
projects: matchSorter(projects, search, { keys }),
projects: searchProjects(projects, search),
};
}, [projects, search]);
+5 -1
View File
@@ -5,6 +5,7 @@ import { appRouter } from "~/services/trcp-router.server";
import { preventCrossOriginCookie } from "~/services/no-cross-origin-cookie";
import { checkCsrf } from "~/services/csrf-session.server";
import { getTrpcResponseMeta } from "~/services/trpc-response-meta.server";
import { isCliApiRequest } from "~/services/trpc-request.server";
const isServiceRequest = (request: Request) => {
return isServiceAuthorization(request.headers.get("Authorization"));
@@ -15,7 +16,10 @@ const isAuthTokenRequest = (request: Request) => {
};
export const action = async ({ request }: ActionFunctionArgs) => {
if (isServiceRequest(request) === false) {
if (
isServiceRequest(request) === false &&
isCliApiRequest(request) === false
) {
preventCrossOriginCookie(request);
if (isAuthTokenRequest(request) === false) {
await checkCsrf(request);
@@ -0,0 +1,27 @@
import { describe, expect, test } from "vitest";
import { apiClientHeader } from "@webstudio-is/trpc-interface/api-compatibility";
import { isCliApiRequest } from "./trpc-request.server";
describe("isCliApiRequest", () => {
test("accepts CLI API requests", () => {
const request = new Request("https://webstudio.is/trpc", {
headers: {
[apiClientHeader]: "cli",
},
});
expect(isCliApiRequest(request)).toBe(true);
});
test("rejects browser and unknown API requests", () => {
const browserRequest = new Request("https://webstudio.is/trpc", {
headers: {
[apiClientHeader]: "browser",
},
});
const unknownRequest = new Request("https://webstudio.is/trpc");
expect(isCliApiRequest(browserRequest)).toBe(false);
expect(isCliApiRequest(unknownRequest)).toBe(false);
});
});
@@ -0,0 +1,5 @@
import { apiClientHeader } from "@webstudio-is/trpc-interface/api-compatibility";
export const isCliApiRequest = (request: Pick<Request, "headers">) => {
return request.headers.get(apiClientHeader) === "cli";
};
+26
View File
@@ -10,6 +10,7 @@ import {
import { createFileIfNotExists, isFileExists } from "../fs-utils";
import { resolveApiConnection } from "../api-connection";
import { sync } from "./sync";
import { apiCompatibilityHeaders } from "./api";
const originalCwd = process.cwd();
let tempDir: string;
@@ -138,6 +139,31 @@ test("downloads project bundle asset files into local project bundle", async ()
expect(indicator.message).toHaveBeenCalledWith("Downloading 1 asset files");
});
test("sends linked share token when synchronizing by build id", async () => {
const resolveApiConnection = vi.fn(async () => ({
authToken: "share-token",
origin: "https://example.com",
projectId: "project-id",
}));
await sync(
{
buildId: "build-1",
},
{
...dependencies,
resolveApiConnection,
}
);
expect(loadProjectBundleByBuildId).toHaveBeenCalledWith({
buildId: "build-1",
authToken: "share-token",
origin: "https://example.com",
headers: apiCompatibilityHeaders,
});
});
test("does not write local data when synchronized asset download fails", async () => {
const assets = [createImageAssetFixture()];
loadProjectBundleByBuildId.mockResolvedValue(createProjectBundle({ assets }));
+30
View File
@@ -70,6 +70,7 @@ import {
listResources,
listTexts,
listVariables,
loadProjectBundleByBuildId,
loadProjectBundleByProjectId,
moveInstance,
publish,
@@ -1012,6 +1013,35 @@ test("uploads assets as binary requests", async () => {
);
});
test("loads project bundle by build id without auth headers", async () => {
const project = createPublishedProjectBundleFixture();
const fetch = vi.fn().mockResolvedValue(
new Response(JSON.stringify([{ result: { data: project } }]), {
headers: {
"content-type": "application/json",
},
})
);
vi.stubGlobal("fetch", fetch);
await loadProjectBundleByBuildId({
buildId: project.build.id,
origin: "https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.apps.webstudio.is",
});
expect(fetch).toHaveBeenCalledOnce();
const [_url, init] = fetch.mock.calls[0] as [URL, RequestInit];
expect(init.headers).toMatchObject({
"content-type": "application/json",
});
expect((init.headers as Record<string, string>)["x-auth-token"]).toBe(
undefined
);
expect((init.headers as Record<string, string>).authorization).toBe(
undefined
);
});
test("reports asset upload errors", async () => {
vi.stubGlobal(
"fetch",
+4 -1
View File
@@ -571,12 +571,15 @@ export const loadProjectBundleByBuildId = async (
serviceToken: string;
}
| { authToken: string }
| { authToken?: undefined; serviceToken?: undefined }
)
): Promise<PublishedProjectBundle> => {
const headers: RequestHeaders =
"serviceToken" in params
? { Authorization: params.serviceToken }
: { "x-auth-token": params.authToken };
: "authToken" in params
? { "x-auth-token": params.authToken }
: {};
const data = await createTrpcClient(params.origin, {
...params.headers,