mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-23 03:27:27 +09:00
chore: Fix/build id sync from main (#5843)
This commit is contained in:
1 parent
a7f1bc6e61
commit
bfcdef65de
10 files changed
+192
-6
No files matched your search
+1
-1
@@ -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,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";
|
||||
};
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user