| 1 | import { sha256 } from "@noble/hashes/sha2"; |
| 2 | import JSZip from "jszip"; |
| 3 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; |
| 4 | |
| 5 | import { |
| 6 | importOpenVsxThemeExtension, |
| 7 | searchOpenVsxThemes, |
| 8 | type OpenVsxThemeExtension, |
| 9 | } from "./openVsxThemes"; |
| 10 | import { getThemeColorsForMode, themeColorToHex } from "./themePalette"; |
| 11 | |
| 12 | const ASSET_ROOT = "https://open-vsx.org/api/demo/theme/1.0.0/file"; |
| 13 | |
| 14 | function extensionDetail(overrides: Record<string, unknown> = {}) { |
| 15 | return { |
| 16 | namespace: "demo", |
| 17 | name: "theme", |
| 18 | displayName: "Demo Theme", |
| 19 | description: "A nice theme", |
| 20 | version: "1.0.0", |
| 21 | downloadCount: 123456, |
| 22 | license: "MIT", |
| 23 | repository: "https://github.com/demo/theme", |
| 24 | files: { |
| 25 | icon: `${ASSET_ROOT}/icon.png`, |
| 26 | manifest: `${ASSET_ROOT}/package.json`, |
| 27 | sha256: `${ASSET_ROOT}/demo.theme-1.0.0.sha256`, |
| 28 | download: `${ASSET_ROOT}/demo.theme-1.0.0.vsix`, |
| 29 | }, |
| 30 | ...overrides, |
| 31 | }; |
| 32 | } |
| 33 | |
| 34 | afterEach(() => { |
| 35 | vi.useRealTimers(); |
| 36 | vi.unstubAllGlobals(); |
| 37 | }); |
| 38 | |
| 39 | describe("Open VSX themes", () => { |
| 40 | it("searches theme extensions and keeps only supported open-source licenses", async () => { |
| 41 | const fetchMock = vi.fn(async (input: RequestInfo | URL) => { |
| 42 | const url = String(input); |
| 43 | if (url.includes("/-/search?")) { |
| 44 | return new Response( |
| 45 | JSON.stringify({ |
| 46 | extensions: [ |
| 47 | { namespace: "demo", name: "theme" }, |
| 48 | { namespace: "icons", name: "theme" }, |
| 49 | { namespace: "oversized", name: "theme" }, |
| 50 | { namespace: "unlicensed", name: "theme" }, |
| 51 | { namespace: "unavailable", name: "theme" }, |
| 52 | { namespace: "closed", name: "theme" }, |
| 53 | { namespace: "huge", name: "theme" }, |
| 54 | ], |
| 55 | }), |
| 56 | { status: 200 }, |
| 57 | ); |
| 58 | } |
| 59 | if (url.endsWith("/icons/theme")) { |
| 60 | return new Response( |
| 61 | JSON.stringify( |
| 62 | extensionDetail({ |
| 63 | namespace: "icons", |
| 64 | files: { |
| 65 | ...extensionDetail().files, |
| 66 | manifest: `${ASSET_ROOT.replace("demo/theme", "icons/theme")}/package.json`, |
| 67 | }, |
| 68 | }), |
| 69 | ), |
| 70 | { status: 200 }, |
| 71 | ); |
| 72 | } |
| 73 | if (url.endsWith("/oversized/theme")) { |
| 74 | return new Response( |
| 75 | JSON.stringify( |
| 76 | extensionDetail({ |
| 77 | namespace: "oversized", |
| 78 | files: { |
| 79 | ...extensionDetail().files, |
| 80 | download: `${ASSET_ROOT.replace("demo/theme", "oversized/theme")}/oversized.vsix`, |
| 81 | }, |
| 82 | }), |
| 83 | ), |
| 84 | { status: 200 }, |
| 85 | ); |
| 86 | } |
| 87 | if (url.endsWith("/unlicensed/theme")) { |
| 88 | return new Response( |
| 89 | JSON.stringify( |
| 90 | extensionDetail({ |
| 91 | namespace: "unlicensed", |
| 92 | files: { |
| 93 | ...extensionDetail().files, |
| 94 | manifest: `${ASSET_ROOT.replace("demo/theme", "unlicensed/theme")}/package.json`, |
| 95 | }, |
| 96 | }), |
| 97 | ), |
| 98 | { status: 200 }, |
| 99 | ); |
| 100 | } |
| 101 | if (url.endsWith("/unavailable/theme")) { |
| 102 | return new Response( |
| 103 | JSON.stringify( |
| 104 | extensionDetail({ |
| 105 | namespace: "unavailable", |
| 106 | files: { |
| 107 | ...extensionDetail().files, |
| 108 | download: `${ASSET_ROOT.replace("demo/theme", "unavailable/theme")}/unavailable.vsix`, |
| 109 | manifest: `${ASSET_ROOT.replace("demo/theme", "unavailable/theme")}/package.json`, |
| 110 | }, |
| 111 | }), |
| 112 | ), |
| 113 | { status: 200 }, |
| 114 | ); |
| 115 | } |
| 116 | if (url.endsWith("/closed/theme")) { |
| 117 | return new Response( |
| 118 | JSON.stringify(extensionDetail({ namespace: "closed", license: "All Rights Reserved" })), |
| 119 | { status: 200 }, |
| 120 | ); |
| 121 | } |
| 122 | if (url.endsWith("/huge/theme")) { |
| 123 | return new Response(JSON.stringify({ padding: "x".repeat(256 * 1024) }), { status: 200 }); |
| 124 | } |
| 125 | if (url.endsWith("/oversized/theme/1.0.0/file/oversized.vsix")) { |
| 126 | return new Response(null, { |
| 127 | headers: { "content-length": String(20 * 1024 * 1024 + 1) }, |
| 128 | status: 200, |
| 129 | }); |
| 130 | } |
| 131 | if (url.endsWith("/unavailable/theme/1.0.0/file/unavailable.vsix")) { |
| 132 | return new Response(null, { status: 404 }); |
| 133 | } |
| 134 | if (url.endsWith(".vsix")) { |
| 135 | return new Response(null, { headers: { "content-length": "1024" }, status: 200 }); |
| 136 | } |
| 137 | if (url.endsWith("/icons/theme/1.0.0/file/package.json")) { |
| 138 | return new Response(JSON.stringify({ contributes: { iconThemes: [{}] } }), { status: 200 }); |
| 139 | } |
| 140 | if (url.endsWith("/unlicensed/theme/1.0.0/file/package.json")) { |
| 141 | return new Response( |
| 142 | JSON.stringify({ contributes: { themes: [{ path: "./theme.json" }] } }), |
| 143 | { status: 200 }, |
| 144 | ); |
| 145 | } |
| 146 | if (url.endsWith("/unavailable/theme/1.0.0/file/package.json")) { |
| 147 | return new Response( |
| 148 | JSON.stringify({ license: "MIT", contributes: { themes: [{ path: "./theme.json" }] } }), |
| 149 | { status: 200 }, |
| 150 | ); |
| 151 | } |
| 152 | if (url.endsWith("/demo/theme/1.0.0/file/package.json")) { |
| 153 | return new Response( |
| 154 | JSON.stringify({ license: "MIT", contributes: { themes: [{ path: "./theme.json" }] } }), |
| 155 | { status: 200 }, |
| 156 | ); |
| 157 | } |
| 158 | return new Response(JSON.stringify(extensionDetail()), { status: 200 }); |
| 159 | }); |
| 160 | vi.stubGlobal("fetch", fetchMock); |
| 161 | |
| 162 | const results = await searchOpenVsxThemes(" dracula "); |
| 163 | |
| 164 | expect(results).toEqual([ |
| 165 | expect.objectContaining({ |
| 166 | id: "demo.theme", |
| 167 | name: "Demo Theme", |
| 168 | publisher: "demo", |
| 169 | downloadCount: 123456, |
| 170 | iconUrl: `${ASSET_ROOT}/icon.png`, |
| 171 | sourceUrl: "https://github.com/demo/theme", |
| 172 | license: "MIT", |
| 173 | }), |
| 174 | ]); |
| 175 | const searchUrl = new URL(String(fetchMock.mock.calls[0]![0])); |
| 176 | expect(searchUrl.searchParams.get("query")).toBe("dracula"); |
| 177 | expect(searchUrl.searchParams.get("category")).toBe("Themes"); |
| 178 | expect(searchUrl.searchParams.get("sortBy")).toBe("downloadCount"); |
| 179 | expect(searchUrl.searchParams.get("sortOrder")).toBe("desc"); |
| 180 | expect(searchUrl.searchParams.get("size")).toBe("16"); |
| 181 | }); |
| 182 | |
| 183 | it("supports sorting theme searches", async () => { |
| 184 | const fetchMock = vi.fn( |
| 185 | async (..._args: unknown[]) => |
| 186 | new Response(JSON.stringify({ extensions: [] }), { status: 200 }), |
| 187 | ); |
| 188 | vi.stubGlobal("fetch", fetchMock); |
| 189 | |
| 190 | await searchOpenVsxThemes("nord", { sortBy: "rating" }); |
| 191 | |
| 192 | const searchUrl = new URL(String(fetchMock.mock.calls[0]![0])); |
| 193 | expect(searchUrl.searchParams.get("sortBy")).toBe("rating"); |
| 194 | expect(searchUrl.searchParams.get("sortOrder")).toBe("desc"); |
| 195 | }); |
| 196 | |
| 197 | it("rejects malformed search responses", async () => { |
| 198 | vi.stubGlobal( |
| 199 | "fetch", |
| 200 | vi.fn(async () => new Response(JSON.stringify({ error: "unavailable" }), { status: 200 })), |
| 201 | ); |
| 202 | |
| 203 | await expect(searchOpenVsxThemes("nord")).rejects.toThrow( |
| 204 | "Open VSX returned an unreadable search response.", |
| 205 | ); |
| 206 | }); |
| 207 | |
| 208 | it("reports an unavailable search when every detail request fails", async () => { |
| 209 | vi.stubGlobal( |
| 210 | "fetch", |
| 211 | vi.fn(async (input: RequestInfo | URL) => { |
| 212 | if (String(input).includes("/-/search?")) { |
| 213 | return new Response( |
| 214 | JSON.stringify({ extensions: [{ namespace: "demo", name: "theme" }] }), |
| 215 | { status: 200 }, |
| 216 | ); |
| 217 | } |
| 218 | return new Response(null, { status: 429 }); |
| 219 | }), |
| 220 | ); |
| 221 | |
| 222 | await expect(searchOpenVsxThemes("dracula")).rejects.toThrow( |
| 223 | "Open VSX theme details are unavailable", |
| 224 | ); |
| 225 | }); |
| 226 | |
| 227 | it("reports an unavailable search when every detail response is malformed", async () => { |
| 228 | vi.stubGlobal( |
| 229 | "fetch", |
| 230 | vi.fn(async (input: RequestInfo | URL) => { |
| 231 | if (String(input).includes("/-/search?")) { |
| 232 | return new Response( |
| 233 | JSON.stringify({ extensions: [{ namespace: "demo", name: "theme" }] }), |
| 234 | { status: 200 }, |
| 235 | ); |
| 236 | } |
| 237 | return new Response("{}", { status: 200 }); |
| 238 | }), |
| 239 | ); |
| 240 | |
| 241 | await expect(searchOpenVsxThemes("dracula")).rejects.toThrow( |
| 242 | "Open VSX theme details are unavailable", |
| 243 | ); |
| 244 | }); |
| 245 | |
| 246 | it("returns no results when every theme package is unavailable", async () => { |
| 247 | vi.stubGlobal( |
| 248 | "fetch", |
| 249 | vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { |
| 250 | const url = String(input); |
| 251 | if (url.includes("/-/search?")) { |
| 252 | return new Response( |
| 253 | JSON.stringify({ extensions: [{ namespace: "demo", name: "theme" }] }), |
| 254 | { status: 200 }, |
| 255 | ); |
| 256 | } |
| 257 | if (url.endsWith("/demo/theme")) { |
| 258 | return new Response(JSON.stringify(extensionDetail()), { status: 200 }); |
| 259 | } |
| 260 | if (init?.method === "HEAD") return new Response(null, { status: 404 }); |
| 261 | return new Response( |
| 262 | JSON.stringify({ license: "MIT", contributes: { themes: [{ path: "./theme.json" }] } }), |
| 263 | { status: 200 }, |
| 264 | ); |
| 265 | }), |
| 266 | ); |
| 267 | |
| 268 | await expect(searchOpenVsxThemes("dracula")).resolves.toEqual([]); |
| 269 | }); |
| 270 | |
| 271 | it("times out a stalled search request", async () => { |
| 272 | vi.useFakeTimers(); |
| 273 | vi.stubGlobal( |
| 274 | "fetch", |
| 275 | vi.fn( |
| 276 | (_input: RequestInfo | URL, init?: RequestInit) => |
| 277 | new Promise<Response>((_resolve, reject) => { |
| 278 | init?.signal?.addEventListener("abort", () => { |
| 279 | reject(new DOMException("The operation was aborted.", "AbortError")); |
| 280 | }); |
| 281 | }), |
| 282 | ), |
| 283 | ); |
| 284 | |
| 285 | const search = expect(searchOpenVsxThemes("dracula")).rejects.toThrow( |
| 286 | "Open VSX took too long to respond", |
| 287 | ); |
| 288 | await vi.advanceTimersByTimeAsync(10_000); |
| 289 | |
| 290 | await search; |
| 291 | }); |
| 292 | |
| 293 | it("downloads a verified VSIX, reads JSONC includes, and pairs contributed variants", async () => { |
| 294 | const zip = new JSZip(); |
| 295 | zip.file("extension/.gitkeep", ""); |
| 296 | // Theme extensions sometimes publish their development dependencies too. |
| 297 | // Those unused files should not prevent importing the small theme payload. |
| 298 | for (let index = 0; index < 3_000; index += 1) { |
| 299 | zip.file(`extension/node_modules/package-${index}.js`, ""); |
| 300 | } |
| 301 | zip.file( |
| 302 | "extension/themes/base.jsonc", |
| 303 | `{ |
| 304 | // inherited workbench colors |
| 305 | "colors": { |
| 306 | "editor.foreground": "#eeeeee", |
| 307 | "focusBorder": "#8b5cf6", |
| 308 | "editor.findMatchBackground": "#663399", |
| 309 | "editor.findMatchForeground": "#ffffff", |
| 310 | "editor.findMatchHighlightBackground": "#445566", |
| 311 | "editor.findMatchHighlightForeground": "#ffffff", |
| 312 | }, |
| 313 | }`, |
| 314 | ); |
| 315 | zip.file( |
| 316 | "extension/themes/demo-dark.json", |
| 317 | `{ |
| 318 | "include": "./base.jsonc", |
| 319 | "colors": { |
| 320 | "editor.background": "#111111", |
| 321 | "editor.findMatchHighlightBackground": "#ffffff20" |
| 322 | } |
| 323 | }`, |
| 324 | ); |
| 325 | zip.file( |
| 326 | "extension/themes/demo-light.json", |
| 327 | `{ |
| 328 | "colors": { |
| 329 | "editor.background": "#fafafa", |
| 330 | "editor.foreground": "#222222", |
| 331 | "focusBorder": "#8b5cf6", |
| 332 | "editor.findMatchBackground": "#335577", |
| 333 | "editor.findMatchForeground": "#ffffff", |
| 334 | "editor.findMatchHighlightBackground": "#bbddff", |
| 335 | "editor.findMatchHighlightForeground": "#112233" |
| 336 | } |
| 337 | }`, |
| 338 | ); |
| 339 | zip.file( |
| 340 | "extension/themes/demo.json", |
| 341 | `{ |
| 342 | "colors": { |
| 343 | "editor.background": "#181818", |
| 344 | "editor.foreground": "#eeeeee" |
| 345 | } |
| 346 | }`, |
| 347 | ); |
| 348 | const packagedManifest = { |
| 349 | publisher: "demo", |
| 350 | name: "theme", |
| 351 | version: "1.0.0", |
| 352 | license: "MIT", |
| 353 | contributes: { |
| 354 | themes: [ |
| 355 | { label: "Demo Dark", uiTheme: "vs-dark", path: "./themes/demo-dark.json" }, |
| 356 | { label: "Demo Light", uiTheme: "vs", path: "./themes/demo-light.json" }, |
| 357 | { label: "Demo", uiTheme: "vs-dark", path: "./themes/demo.json" }, |
| 358 | ], |
| 359 | }, |
| 360 | }; |
| 361 | let packageBytes = new ArrayBuffer(0); |
| 362 | let checksum = ""; |
| 363 | const rebuildPackage = async () => { |
| 364 | zip.file("extension/package.json", JSON.stringify(packagedManifest)); |
| 365 | packageBytes = await zip.generateAsync({ |
| 366 | type: "arraybuffer", |
| 367 | comment: `PK\u0005\u0006${"x".repeat(26)}`, |
| 368 | }); |
| 369 | checksum = [...sha256(new Uint8Array(packageBytes))] |
| 370 | .map((byte) => byte.toString(16).padStart(2, "0")) |
| 371 | .join(""); |
| 372 | }; |
| 373 | await rebuildPackage(); |
| 374 | // The public manifest is only a preflight hint. Imports must use the manifest |
| 375 | // inside the checksummed VSIX rather than this inconsistent contribution. |
| 376 | const manifest = { |
| 377 | contributes: { themes: [{ label: "Wrong", path: "./themes/missing.json" }] }, |
| 378 | }; |
| 379 | const extension: OpenVsxThemeExtension = { |
| 380 | collectionId: "open-vsx:demo.theme", |
| 381 | id: "demo.theme", |
| 382 | name: "Demo Theme", |
| 383 | publisher: "demo", |
| 384 | description: "", |
| 385 | downloadCount: 1, |
| 386 | iconUrl: null, |
| 387 | sourceUrl: null, |
| 388 | license: "MIT", |
| 389 | manifestUrl: `${ASSET_ROOT}/package.json`, |
| 390 | sha256Url: `${ASSET_ROOT}/demo.theme-1.0.0.sha256`, |
| 391 | version: "1.0.0", |
| 392 | vsixUrl: `${ASSET_ROOT}/demo.theme-1.0.0.vsix`, |
| 393 | }; |
| 394 | vi.stubGlobal( |
| 395 | "fetch", |
| 396 | vi.fn(async (input: string | URL | Request) => { |
| 397 | const url = String(input); |
| 398 | if (url === extension.manifestUrl) { |
| 399 | return new Response(JSON.stringify(manifest), { status: 200 }); |
| 400 | } |
| 401 | if (url === extension.sha256Url) return new Response(`${checksum} demo.vsix`); |
| 402 | return new Response(packageBytes, { |
| 403 | status: 200, |
| 404 | headers: { "Content-Length": String(packageBytes.byteLength) }, |
| 405 | }); |
| 406 | }), |
| 407 | ); |
| 408 | |
| 409 | const themes = await importOpenVsxThemeExtension(extension); |
| 410 | |
| 411 | expect(themes).toHaveLength(2); |
| 412 | expect(new Set(themes.map((theme) => theme.id)).size).toBe(2); |
| 413 | expect(themes.every((theme) => /^ovx-[a-z0-9-]+-[0-9a-f]{12}$/.test(theme.id))).toBe(true); |
| 414 | expect(themes.every((theme) => theme.collection?.id === "open-vsx:demo.theme")).toBe(true); |
| 415 | const paired = themes.find( |
| 416 | (theme) => |
| 417 | getThemeColorsForMode(theme, "light") !== null && |
| 418 | getThemeColorsForMode(theme, "dark") !== null, |
| 419 | )!; |
| 420 | expect(paired.label).toBe("Demo"); |
| 421 | expect(themeColorToHex(paired.colors.canvas)).toBe("#fafafa"); |
| 422 | expect(themeColorToHex(getThemeColorsForMode(paired, "dark")!.canvas)).toBe("#111111"); |
| 423 | expect(themeColorToHex(getThemeColorsForMode(paired, "dark")!.text)).toBe("#eeeeee"); |
| 424 | const darkColors = getThemeColorsForMode(paired, "dark")!; |
| 425 | expect(themeColorToHex(darkColors.searchMatchActiveBackground)).toBe("#663399"); |
| 426 | expect(themeColorToHex(darkColors.searchMatchActiveForeground)).toBe("#ffffff"); |
| 427 | expect(themeColorToHex(darkColors.searchMatchBackground)).toBe("#2f2f2f"); |
| 428 | expect(themeColorToHex(darkColors.searchMatchForeground)).toBe("#ffffff"); |
| 429 | expect(themeColorToHex(paired.colors.searchMatchActiveBackground)).toBe("#335577"); |
| 430 | expect(themeColorToHex(paired.colors.searchMatchActiveForeground)).toBe("#ffffff"); |
| 431 | expect(themeColorToHex(paired.colors.searchMatchBackground)).toBe("#bbddff"); |
| 432 | expect(themeColorToHex(paired.colors.searchMatchForeground)).toBe("#112233"); |
| 433 | |
| 434 | packagedManifest.contributes.themes[0]!.label = "Renamed Dark"; |
| 435 | packagedManifest.contributes.themes[1]!.label = "Renamed Light"; |
| 436 | packagedManifest.contributes.themes[2]!.label = "Renamed Solo"; |
| 437 | await rebuildPackage(); |
| 438 | const renamedThemes = await importOpenVsxThemeExtension(extension); |
| 439 | expect(renamedThemes.map((theme) => theme.id)).toEqual(themes.map((theme) => theme.id)); |
| 440 | |
| 441 | packagedManifest.contributes.themes.push({ |
| 442 | label: "Duplicate Demo", |
| 443 | uiTheme: "vs-dark", |
| 444 | path: "./themes/demo.json", |
| 445 | }); |
| 446 | await rebuildPackage(); |
| 447 | const duplicatePathThemes = await importOpenVsxThemeExtension(extension); |
| 448 | expect(new Set(duplicatePathThemes.map((theme) => theme.id)).size).toBe( |
| 449 | duplicatePathThemes.length, |
| 450 | ); |
| 451 | expect(themes.every(({ id }) => duplicatePathThemes.some((theme) => theme.id === id))).toBe( |
| 452 | true, |
| 453 | ); |
| 454 | packagedManifest.contributes.themes.pop(); |
| 455 | |
| 456 | packagedManifest.contributes.themes[2]!.path = "./themes/missing.json"; |
| 457 | await rebuildPackage(); |
| 458 | await expect(importOpenVsxThemeExtension(extension)).rejects.toThrow( |
| 459 | "could not be imported safely", |
| 460 | ); |
| 461 | packagedManifest.contributes.themes[2]!.path = "./themes/demo.json"; |
| 462 | |
| 463 | packagedManifest.license = "Proprietary"; |
| 464 | await rebuildPackage(); |
| 465 | await expect(importOpenVsxThemeExtension(extension)).rejects.toThrow( |
| 466 | "does not match its advertised license", |
| 467 | ); |
| 468 | |
| 469 | Reflect.deleteProperty(packagedManifest, "license"); |
| 470 | await rebuildPackage(); |
| 471 | await expect(importOpenVsxThemeExtension(extension)).rejects.toThrow( |
| 472 | "does not match its advertised license", |
| 473 | ); |
| 474 | }); |
| 475 | |
| 476 | it("stops import work when the request is cancelled", async () => { |
| 477 | const packageBytes = new Uint8Array([1, 2, 3]); |
| 478 | const checksum = [...sha256(packageBytes)] |
| 479 | .map((byte) => byte.toString(16).padStart(2, "0")) |
| 480 | .join(""); |
| 481 | const controller = new AbortController(); |
| 482 | const extension: OpenVsxThemeExtension = { |
| 483 | collectionId: "open-vsx:demo.theme", |
| 484 | id: "demo.theme", |
| 485 | name: "Demo Theme", |
| 486 | publisher: "demo", |
| 487 | description: "", |
| 488 | downloadCount: 1, |
| 489 | iconUrl: null, |
| 490 | sourceUrl: null, |
| 491 | license: "MIT", |
| 492 | manifestUrl: `${ASSET_ROOT}/package.json`, |
| 493 | sha256Url: `${ASSET_ROOT}/demo.theme-1.0.0.sha256`, |
| 494 | version: "1.0.0", |
| 495 | vsixUrl: `${ASSET_ROOT}/demo.theme-1.0.0.vsix`, |
| 496 | }; |
| 497 | vi.stubGlobal( |
| 498 | "fetch", |
| 499 | vi.fn(async (input: string | URL | Request) => { |
| 500 | const url = String(input); |
| 501 | if (url === extension.manifestUrl) { |
| 502 | return new Response( |
| 503 | JSON.stringify({ contributes: { themes: [{ path: "./theme.json" }] } }), |
| 504 | ); |
| 505 | } |
| 506 | if (url === extension.sha256Url) { |
| 507 | controller.abort(); |
| 508 | return new Response(checksum); |
| 509 | } |
| 510 | return new Response(packageBytes); |
| 511 | }), |
| 512 | ); |
| 513 | |
| 514 | await expect(importOpenVsxThemeExtension(extension, controller.signal)).rejects.toMatchObject({ |
| 515 | name: "AbortError", |
| 516 | }); |
| 517 | }); |
| 518 | |
| 519 | it("rejects a package whose Open VSX checksum does not match", async () => { |
| 520 | const extension: OpenVsxThemeExtension = { |
| 521 | collectionId: "open-vsx:demo.theme", |
| 522 | id: "demo.theme", |
| 523 | name: "Demo Theme", |
| 524 | publisher: "demo", |
| 525 | description: "", |
| 526 | downloadCount: 1, |
| 527 | iconUrl: null, |
| 528 | sourceUrl: null, |
| 529 | license: "MIT", |
| 530 | manifestUrl: `${ASSET_ROOT}/package.json`, |
| 531 | sha256Url: `${ASSET_ROOT}/demo.theme-1.0.0.sha256`, |
| 532 | version: "1.0.0", |
| 533 | vsixUrl: `${ASSET_ROOT}/demo.theme-1.0.0.vsix`, |
| 534 | }; |
| 535 | vi.stubGlobal( |
| 536 | "fetch", |
| 537 | vi.fn(async (input: string | URL | Request) => { |
| 538 | const url = String(input); |
| 539 | if (url === extension.manifestUrl) { |
| 540 | return new Response( |
| 541 | JSON.stringify({ contributes: { themes: [{ path: "./theme.json" }] } }), |
| 542 | ); |
| 543 | } |
| 544 | if (url === extension.sha256Url) return new Response("0".repeat(64)); |
| 545 | return new Response(new Uint8Array([1, 2, 3])); |
| 546 | }), |
| 547 | ); |
| 548 | |
| 549 | await expect(importOpenVsxThemeExtension(extension)).rejects.toThrow("integrity check"); |
| 550 | }); |
| 551 | }); |