manifest-parity.test.ts

4.4 KB · raw

1// PB-26-610 — the source manifests are security-reviewed; the built manifests2// are what ships. @crxjs/vite-plugin rewrites them at build time, so a change3// to the plugin or to vite.config.ts could add a permission or weaken the CSP4// in the artifact without touching the reviewed source. This pins the security5// properties across that transform, naming every expected rewrite.67import { existsSync, readFileSync } from "node:fs";8import { resolve } from "node:path";9import { describe, expect, it } from "vitest";1011type ContentScript = {12  matches?: string[];13  js?: string[];14  run_at?: string;15  all_frames?: boolean;16};1718type ExtensionManifest = {19  version?: string;20  permissions?: string[];21  host_permissions?: string[];22  content_security_policy?: Record<string, string>;23  content_scripts?: ContentScript[];24  background?: {25    service_worker?: string;26    scripts?: string[];27    type?: string;28  };29};3031/** @crxjs/vite-plugin injects this; the sibling WAR test asserts its scope. */32const CRXJS_ADDED_KEYS = ["web_accessible_resources"];3334const sourcePath = (target: "chrome" | "firefox") =>35  resolve(process.cwd(), target === "chrome" ? "manifest.json" : "manifest.firefox.json");36const builtPath = (target: "chrome" | "firefox") =>37  resolve(process.cwd(), `dist-${target}`, "manifest.json");3839function read(path: string): ExtensionManifest {40  return JSON.parse(readFileSync(path, "utf8")) as ExtensionManifest;41}4243for (const target of ["chrome", "firefox"] as const) {44  describe.skipIf(!existsSync(builtPath(target)))(45    `built ${target} manifest matches the reviewed source manifest`,46    () => {47      it("adds no permission and no host permission", () => {48        const source = read(sourcePath(target));49        const built = read(builtPath(target));5051        expect(built.permissions ?? []).toEqual(source.permissions ?? []);52        expect(built.host_permissions ?? []).toEqual(source.host_permissions ?? []);53      });5455      it("keeps the content security policy byte-identical", () => {56        const source = read(sourcePath(target));57        const built = read(builtPath(target));5859        expect(built.content_security_policy).toEqual(source.content_security_policy);60      });6162      it("keeps content_scripts matches, run_at and frame scope identical", () => {63        const source = read(sourcePath(target));64        const built = read(builtPath(target));6566        const shape = (scripts: ContentScript[] | undefined) =>67          (scripts ?? []).map((script) => ({68            matches: script.matches,69            run_at: script.run_at,70            all_frames: script.all_frames,71            scriptCount: script.js?.length ?? 0,72          }));7374        expect(shape(built.content_scripts)).toEqual(shape(source.content_scripts));75      });7677      it("keeps the same background kind and module type", () => {78        const source = read(sourcePath(target));79        const built = read(builtPath(target));8081        expect(Object.keys(built.background ?? {}).sort()).toEqual(82          Object.keys(source.background ?? {}).sort(),83        );84        expect(built.background?.type).toBe(source.background?.type);85      });8687      it("introduces no top-level key beyond the known crxjs addition", () => {88        const source = read(sourcePath(target));89        const built = JSON.parse(readFileSync(builtPath(target), "utf8")) as Record<90          string,91          unknown92        >;9394        const added = Object.keys(built).filter(95          (key) => !(key in source) && !CRXJS_ADDED_KEYS.includes(key),96        );97        expect(added).toEqual([]);98      });99    },100  );101}102103// The shipped extension version comes from the source manifests104// (scripts/release.ts reads manifest.json), while package.json drives105// __PEACH_VERSION__ and release-note selection. When they disagree, a release106// publishes a store version that does not match the product version the app107// reports. Parity held through v5.2.30 and then silently drifted for eight108// saves, so it is pinned here rather than left to discipline.109describe("product version parity across release inputs", () => {110  it("package.json and both source manifests carry the same version", () => {111    const pkg = JSON.parse(112      readFileSync(resolve(process.cwd(), "package.json"), "utf8"),113    ) as { version?: string };114115    expect(pkg.version).toMatch(/^\d+\.\d+\.\d+$/);116    expect(read(sourcePath("chrome")).version).toBe(pkg.version);117    expect(read(sourcePath("firefox")).version).toBe(pkg.version);118  });119});