diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index 9c2ee334bc..8053151cdb 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -10,6 +10,7 @@ import { getIdFromUrl, createImage, generateImageThumbnailUrl, + getImageData, } from "../data/image_upload"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; @@ -197,8 +198,7 @@ export class HaPictureUpload extends LitElement { const url = generateImageThumbnailUrl(mediaId, undefined, true); let data; try { - const response = await fetch(url); - data = await response.blob(); + data = await getImageData(url); } catch (err: any) { showAlertDialog(this, { text: err.toString(), diff --git a/src/data/image_upload.ts b/src/data/image_upload.ts index dbf591a62e..2ea6af4ed1 100644 --- a/src/data/image_upload.ts +++ b/src/data/image_upload.ts @@ -80,3 +80,17 @@ export const deleteImage = (hass: HomeAssistant, id: string) => type: "image/delete", image_id: id, }); + +export const getImageData = async (url: string) => { + const response = await fetch(url); + + if (!response.ok) { + throw new Error( + `Failed to fetch image: ${ + response.statusText ? response.statusText : response.status + }` + ); + } + + return response.blob(); +}; diff --git a/test/data/image_upload.test.ts b/test/data/image_upload.test.ts new file mode 100644 index 0000000000..805cc86b8b --- /dev/null +++ b/test/data/image_upload.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { + getIdFromUrl, + generateImageThumbnailUrl, + fetchImages, + createImage, + updateImage, + deleteImage, + getImageData, + URL_PREFIX, + MEDIA_PREFIX, +} from "../../src/data/image_upload"; +import type { HomeAssistant } from "../../src/types"; + +describe("image_upload", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("getIdFromUrl", () => { + it("should extract id from URL_PREFIX", () => { + const url = `${URL_PREFIX}12345/some/path`; + const id = getIdFromUrl(url); + expect(id).toBe("12345"); + }); + + it("should extract id from MEDIA_PREFIX", () => { + const url = `${MEDIA_PREFIX}/12345`; + const id = getIdFromUrl(url); + expect(id).toBe("12345"); + }); + + it("should return undefined for invalid url", () => { + const url = "invalid_url"; + const id = getIdFromUrl(url); + expect(id).toBeUndefined(); + }); + }); + + describe("generateImageThumbnailUrl", () => { + it("should generate thumbnail URL with size", () => { + const url = generateImageThumbnailUrl("12345", 100); + expect(url).toBe("/api/image/serve/12345/100x100"); + }); + + it("should generate original image URL", () => { + const url = generateImageThumbnailUrl("12345", undefined, true); + expect(url).toBe("/api/image/serve/12345/original"); + }); + + it("should throw error if size is not provided and original is false", () => { + expect(() => generateImageThumbnailUrl("12345")).toThrow( + "Size must be provided if original is false" + ); + }); + }); + + describe("fetchImages", () => { + it("should fetch images", async () => { + const hass = { + callWS: vi.fn().mockResolvedValue([]), + } as unknown as HomeAssistant; + const images = await fetchImages(hass); + expect(hass.callWS).toHaveBeenCalledWith({ type: "image/list" }); + expect(images).toEqual([]); + }); + }); + + describe("createImage", () => { + it("should create an image", async () => { + const file = new File([""], "image.png", { type: "image/png" }); + const hass = { + fetchWithAuth: vi.fn().mockResolvedValue({ + status: 200, + json: vi.fn().mockResolvedValue({ id: "12345" }), + }), + } as unknown as HomeAssistant; + const image = await createImage(hass, file); + expect(hass.fetchWithAuth).toHaveBeenCalled(); + expect(image).toEqual({ id: "12345" }); + }); + + it("should throw error if image is too large", async () => { + const file = new File([""], "image.png", { type: "image/png" }); + const hass = { + fetchWithAuth: vi.fn().mockResolvedValue({ status: 413 }), + } as unknown as HomeAssistant; + await expect(createImage(hass, file)).rejects.toThrow( + "Uploaded image is too large (image.png)" + ); + }); + + it("should throw error if fetch fails", async () => { + const file = new File([""], "image.png", { type: "image/png" }); + const hass = { + fetchWithAuth: vi.fn().mockResolvedValue({ status: 500 }), + } as unknown as HomeAssistant; + await expect(createImage(hass, file)).rejects.toThrow("Unknown error"); + }); + }); + + describe("updateImage", () => { + it("should update an image", async () => { + const hass = { + callWS: vi.fn().mockResolvedValue({}), + } as unknown as HomeAssistant; + const updates = { name: "new name" }; + await updateImage(hass, "12345", updates); + expect(hass.callWS).toHaveBeenCalledWith({ + type: "image/update", + media_id: "12345", + ...updates, + }); + }); + }); + + describe("deleteImage", () => { + it("should delete an image", async () => { + const hass = { + callWS: vi.fn().mockResolvedValue({}), + } as unknown as HomeAssistant; + await deleteImage(hass, "12345"); + expect(hass.callWS).toHaveBeenCalledWith({ + type: "image/delete", + image_id: "12345", + }); + }); + }); + + describe("getImageData", () => { + it("should fetch image data", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + blob: vi.fn().mockResolvedValue(new Blob()), + }); + const data = await getImageData("http://example.com/image.png"); + expect(global.fetch).toHaveBeenCalledWith("http://example.com/image.png"); + expect(data).toBeInstanceOf(Blob); + }); + + it("should throw error if fetch fails", async () => { + global.fetch = vi + .fn() + .mockResolvedValue({ ok: false, statusText: "Not Found" }); + await expect( + getImageData("http://example.com/image.png") + ).rejects.toThrow("Failed to fetch image: Not Found"); + }); + }); +});