feat: add host-local custom project icons (#2335)
* feat(projects): add local icons and colors * refactor(projects): simplify appearance handling * fix(projects): serialize appearance saves * test(app): update project appearance expectations * test: fix project appearance CI expectations * refactor(projects): organize appearance code * fix(server): adopt main's reconcile model in project appearance updates * fix(projects): preserve host-local appearance targets * fix(desktop): restore Linux development startup Ubuntu can block Electron's unprivileged user-namespace fallback while the npm-installed sandbox helper is not SUID. Disable the Chromium process sandbox only in the Linux development runner so packaged desktop policy remains unchanged. * feat(projects): add custom project icons Keep URL acquisition in the client so the host daemon only validates uploaded bytes. Project name and icon changes share one adaptive edit sheet. * refactor(settings): scope projects by host --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
706ae3d9bb
commit
800bd2bef1
@@ -0,0 +1,46 @@
|
||||
import { Buffer } from "buffer";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { importProjectIconFromUrl } from "./import-project-icon";
|
||||
|
||||
const IMAGE = new Uint8Array([1, 2, 3, 4]);
|
||||
|
||||
function response(body: BodyInit, contentType: string): Response {
|
||||
return new Response(body, { headers: { "content-type": contentType } });
|
||||
}
|
||||
|
||||
describe("project icon URL import", () => {
|
||||
it("turns a direct image URL into an upload payload", async () => {
|
||||
const source = await importProjectIconFromUrl("https://example.com/icon.png", async () =>
|
||||
response(IMAGE.buffer, "image/png"),
|
||||
);
|
||||
|
||||
expect(source).toEqual({ type: "upload", data: Buffer.from(IMAGE).toString("base64") });
|
||||
});
|
||||
|
||||
it("fetches a website icon on the client and returns an upload payload", async () => {
|
||||
const requested: string[] = [];
|
||||
const source = await importProjectIconFromUrl("https://example.com", async (input) => {
|
||||
const url = input.toString();
|
||||
requested.push(url);
|
||||
if (url === "https://example.com/") {
|
||||
return response('<link rel="icon" href="/brand.png">', "text/html");
|
||||
}
|
||||
return response(IMAGE.buffer, "image/png");
|
||||
});
|
||||
|
||||
expect(requested).toEqual(["https://example.com/", "https://example.com/brand.png"]);
|
||||
expect(source.type).toBe("upload");
|
||||
});
|
||||
|
||||
it("rejects non-HTTP URLs without fetching", async () => {
|
||||
let fetched = false;
|
||||
|
||||
await expect(
|
||||
importProjectIconFromUrl("file:///etc/passwd", async () => {
|
||||
fetched = true;
|
||||
return response(IMAGE.buffer, "image/png");
|
||||
}),
|
||||
).rejects.toThrow("URL must use HTTP or HTTPS without credentials");
|
||||
expect(fetched).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user