Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/builder/app/routes/_ui.$.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { loader } from "./_ui.$";

describe("_ui.$ loader", () => {
afterEach(() => {
vi.restoreAllMocks();
});

test("returns 404 for missing apple touch icons without cross-origin logging", async () => {
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {});
const request = new Request(
"https://p-project.apps.webstudio.is/apple-touch-icon-precomposed.png",
{
headers: {
accept: "*/*",
},
}
);

await expect(
loader({
request,
params: {},
context: {},
})
).rejects.toMatchObject({
status: 404,
});
expect(consoleInfo).not.toHaveBeenCalled();
});
});
8 changes: 4 additions & 4 deletions apps/builder/app/routes/_ui.$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import { preventCrossOriginCookie } from "~/services/no-cross-origin-cookie";
export { ErrorBoundary } from "~/shared/error/error-boundary";

export const loader = async ({ request }: LoaderFunctionArgs) => {
preventCrossOriginCookie(request);

// No data to protect with CSRF token

const url = new URL(request.url);

// Redirecting asset files (e.g., .js, .css) to the dashboard should be avoided.
Expand All @@ -28,6 +24,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
});
}

preventCrossOriginCookie(request);

// No data to protect with CSRF token

const contentType = request.headers.get("Content-Type");

if (contentType?.includes("application/json")) {
Expand Down
10 changes: 10 additions & 0 deletions apps/builder/app/routes/rest.assets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, test } from "vitest";
import { MaxAssetsPerProjectError } from "@webstudio-is/asset-uploader/index.server";

describe("MaxAssetsPerProjectError", () => {
test("can be classified by type", () => {
expect(new MaxAssetsPerProjectError(100)).toBeInstanceOf(
MaxAssetsPerProjectError
);
});
});
11 changes: 9 additions & 2 deletions apps/builder/app/routes/rest.assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Asset } from "@webstudio-is/sdk";
import {
loadAssetsByProject,
createUploadName,
MaxAssetsPerProjectError,
} from "@webstudio-is/asset-uploader/index.server";
import { createContext } from "~/shared/context.server";
import { preventCrossOriginCookie } from "~/services/no-cross-origin-cookie";
Expand Down Expand Up @@ -61,10 +62,16 @@ export const action = async (props: ActionFunctionArgs) => {
};
}
} catch (error) {
console.error(error);
const parsedError = parseError(error);

if (error instanceof MaxAssetsPerProjectError) {
console.info(error);
} else {
console.error(error);
}

return {
errors: parseError(error).message,
errors: parsedError.message,
};
}
};
30 changes: 30 additions & 0 deletions apps/builder/app/services/no-cross-origin-cookie.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { preventCrossOriginCookie } from "./no-cross-origin-cookie";

describe("preventCrossOriginCookie", () => {
afterEach(() => {
vi.restoreAllMocks();
});

test("logs blocked cross-origin requests as info", () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {});
const request = new Request("https://apps.webstudio.is/rest/data", {
method: "POST",
headers: {
cookie: "session=1",
},
});

expect(() => preventCrossOriginCookie(request)).toThrow();

expect(consoleError).not.toHaveBeenCalled();
expect(consoleInfo).toHaveBeenCalledWith(
"Blocked cross-origin request to https://apps.webstudio.is/rest/data",
[]
);
expect(request.headers.has("cookie")).toBe(false);
});
});
2 changes: 1 addition & 1 deletion apps/builder/app/services/no-cross-origin-cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const preventCrossOriginCookie = (
}

if (throwError) {
console.error(`Cross-origin request to ${request.url} blocked`, [
console.info(`Blocked cross-origin request to ${request.url}`, [
...request.headers.entries(),
]);

Expand Down
1 change: 1 addition & 0 deletions apps/builder/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default defineConfig(({ mode }) => {
plugins: [
remix({
presets: [vercelPreset()],
ignoredRouteFiles: ["**/.*", "**/*.test.{ts,tsx}"],
future: {
v3_lazyRouteDiscovery: false,
v3_relativeSplatPath: false,
Expand Down
6 changes: 0 additions & 6 deletions packages/asset-uploader/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,3 @@ export const MaxSize: z.ZodEffects<
.default(MAX_UPLOAD_SIZE)
// user inputs the max value in mb and we transform it to bytes
.transform(toBytes);

export const MaxAssets: z.ZodEffects<
z.ZodDefault<z.ZodString>,
number,
string | undefined
> = z.string().default("50").transform(Number.parseFloat);
13 changes: 10 additions & 3 deletions packages/asset-uploader/src/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ type UploadData = {
maxAssetsPerProject: number;
};

export class MaxAssetsPerProjectError extends Error {
constructor(maxAssetsPerProject: number) {
super(
`The maximum number of assets per project is ${maxAssetsPerProject}.`
);
this.name = "MaxAssetsPerProjectError";
}
}

const UPLOADING_STALE_TIMEOUT = 1000 * 60 * 30; // 30 minutes

export const createUploadName = async (
Expand Down Expand Up @@ -65,9 +74,7 @@ export const createUploadName = async (
* it's probable that the user can exceed the limit a little bit.
* So it can be a little bit strange that the limit is 5 but the user already has 7.
**/
throw new Error(
`The maximum number of assets per project is ${maxAssetsPerProject}.`
);
throw new MaxAssetsPerProjectError(maxAssetsPerProject);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/plans/src/plan-features.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ describe("parsePlansEnv", () => {
{ name: "Workspaces", features: { ...fullFeatures, maxWorkspaces: 10 } },
]);

test("default plan allows 100 assets per project", () => {
expect(defaultPlanFeatures.maxAssetsPerProject).toBe(100);
});

test("returns empty map for empty JSON array", () => {
expect(parsePlansEnv("[]").size).toBe(0);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/plans/src/plan-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const defaultPlanFeatures: PlanFeatures = {
maxDailyPublishesPerUser: 10,
maxWorkspaces: 1,
maxProjectsAllowedPerUser: 100,
maxAssetsPerProject: 50,
maxAssetsPerProject: 100,
seatsIncluded: 0,
maxSeatsPerWorkspace: 0,
};
Expand Down
47 changes: 46 additions & 1 deletion packages/sdk/src/resource-loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { describe, expect, test, beforeEach, vi, type Mock } from "vitest";
import {
afterEach,
describe,
expect,
test,
beforeEach,
vi,
type Mock,
} from "vitest";

import { loadResource } from "./resource-loader";
import type { ResourceRequest } from "./schema/resources";
Expand All @@ -13,6 +21,10 @@ describe("loadResource", () => {
vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

test("should successfully fetch a resource and return a JSON response", async () => {
const mockResponse = new Response(JSON.stringify({ key: "value" }), {
status: 200,
Expand Down Expand Up @@ -156,4 +168,37 @@ describe("loadResource", () => {
]),
});
});

test("should log failed resource responses as info", async () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {});
const mockResponse = new Response("not found", {
status: 404,
});
mockFetch.mockResolvedValue(mockResponse);

const resourceRequest: ResourceRequest = {
name: "resource",
url: "https://example.com/resource",
searchParams: [],
method: "get",
headers: [],
body: undefined,
};

const result = await loadResource(mockFetch, resourceRequest);

expect(result).toEqual({
data: "not found",
ok: false,
status: 404,
statusText: "",
});
expect(consoleError).not.toHaveBeenCalled();
expect(consoleInfo).toHaveBeenCalledWith(
'Failed to load resource: https://example.com/resource - 404: "not found"'
);
});
});
2 changes: 1 addition & 1 deletion packages/sdk/src/resource-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const loadResource = async (
}

if (!response.ok) {
console.error(
console.info(
`Failed to load resource: ${href} - ${response.status}: ${JSON.stringify(data).slice(0, 300)}`
);
}
Expand Down
Loading