305 lines
11 KiB
JavaScript
305 lines
11 KiB
JavaScript
import { createHmac, createVerify, generateKeyPairSync } from "node:crypto";
|
|
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
amoUploadReady,
|
|
amoValidationErrors,
|
|
assertCredentials,
|
|
assertOfficialRepository,
|
|
buildApprovalNotes,
|
|
chromeSubmissionPlan,
|
|
chromeUploadFinished,
|
|
compareVersions,
|
|
createAmoJwt,
|
|
createChromeClient,
|
|
createFirefoxClient,
|
|
createGoogleServiceAccountAssertion,
|
|
credentialPresence,
|
|
firefoxSubmissionPlan,
|
|
FIREFOX_LICENSE_SLUG,
|
|
highestCrxVersion,
|
|
OFFICIAL_REPOSITORY,
|
|
parsePublishArgs,
|
|
resolveReleaseNotes,
|
|
shouldCopyExtensionSource,
|
|
workspacePackageManifests,
|
|
} from "./publish-web-clipper.mjs";
|
|
|
|
const workflow = readFileSync(
|
|
new URL("../.github/workflows/extension-store.yml", import.meta.url),
|
|
"utf8",
|
|
);
|
|
|
|
describe("web clipper store command", () => {
|
|
test("defaults to both stores", () => {
|
|
expect(parsePublishArgs([])).toMatchObject({
|
|
platform: "both",
|
|
dryRun: false,
|
|
});
|
|
});
|
|
|
|
test("rejects an unknown store", () => {
|
|
expect(() => parsePublishArgs(["--platform", "edge"])).toThrow("--platform");
|
|
});
|
|
|
|
test("compares numeric versions", () => {
|
|
expect(compareVersions("0.1.9", "0.1.8")).toBeGreaterThan(0);
|
|
expect(compareVersions("0.1.8", "0.1.8")).toBe(0);
|
|
expect(compareVersions("0.2.0", "0.1.9")).toBeGreaterThan(0);
|
|
expect(() => compareVersions("1", "0.1.8")).toThrow("X.Y.Z");
|
|
});
|
|
|
|
test("blocks a Chrome package that is not newer than the public version", () => {
|
|
const plan = chromeSubmissionPlan({
|
|
packageVersion: "0.1.8",
|
|
status: {
|
|
publishedItemRevisionStatus: {
|
|
state: "PUBLISHED",
|
|
distributionChannels: [{ crxVersion: "0.1.8", deployPercentage: 100 }],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(plan.action).toBe("blocked");
|
|
expect(plan.reason).toContain("0.1.8");
|
|
});
|
|
|
|
test("uploads a newer Chrome package when nothing is pending", () => {
|
|
expect(chromeSubmissionPlan({
|
|
packageVersion: "0.1.9",
|
|
status: {
|
|
publishedItemRevisionStatus: {
|
|
distributionChannels: [{ crxVersion: "0.1.8" }],
|
|
},
|
|
},
|
|
}).action).toBe("upload");
|
|
});
|
|
|
|
test("treats the same Chrome version awaiting review as already submitted", () => {
|
|
expect(chromeSubmissionPlan({
|
|
packageVersion: "0.1.9",
|
|
status: {
|
|
publishedItemRevisionStatus: {
|
|
distributionChannels: [{ crxVersion: "0.1.8" }],
|
|
},
|
|
submittedItemRevisionStatus: {
|
|
state: "PENDING_REVIEW",
|
|
distributionChannels: [{ crxVersion: "0.1.9" }],
|
|
},
|
|
},
|
|
}).action).toBe("already-submitted");
|
|
});
|
|
|
|
test("blocks a different Chrome version that is already pending", () => {
|
|
expect(chromeSubmissionPlan({
|
|
packageVersion: "0.2.0",
|
|
status: {
|
|
submittedItemRevisionStatus: {
|
|
state: "STAGED",
|
|
distributionChannels: [{ crxVersion: "0.1.9" }],
|
|
},
|
|
},
|
|
}).action).toBe("blocked");
|
|
});
|
|
|
|
test("uses the highest Chrome channel version", () => {
|
|
expect(highestCrxVersion({
|
|
distributionChannels: [
|
|
{ crxVersion: "0.1.8" },
|
|
{ crxVersion: "0.1.10" },
|
|
],
|
|
})).toBe("0.1.10");
|
|
});
|
|
|
|
test("asks Firefox to attach source when the pending version has none", () => {
|
|
expect(firefoxSubmissionPlan({
|
|
packageVersion: "0.1.9",
|
|
versions: [{ version: "0.1.9", file: { status: "unreviewed" }, source: null }],
|
|
}).action).toBe("attach-source");
|
|
});
|
|
|
|
test("does not reuse a public Firefox version", () => {
|
|
expect(firefoxSubmissionPlan({
|
|
packageVersion: "0.1.8",
|
|
versions: [{ version: "0.1.8", file: { status: "public" }, source: "https://example.test/source" }],
|
|
}).action).toBe("blocked");
|
|
});
|
|
|
|
test("blocks a second Firefox upload while another version awaits review", () => {
|
|
expect(firefoxSubmissionPlan({
|
|
packageVersion: "0.1.10",
|
|
versions: [{ version: "0.1.9", file: { status: "unreviewed" }, source: "https://example.test/source" }],
|
|
}).reason).toContain("0.1.9");
|
|
});
|
|
|
|
test("reads a finished Chrome upload without treating every status item as finished", () => {
|
|
expect(chromeUploadFinished({ itemId: "abc" })).toMatchObject({ done: true, ok: true });
|
|
expect(chromeUploadFinished({ uploadState: "IN_PROGRESS" }).done).toBe(false);
|
|
expect(chromeUploadFinished({ uploadState: "FAILED" })).toMatchObject({ done: true, ok: false });
|
|
});
|
|
|
|
test("waits until Firefox validation finishes and keeps warnings", () => {
|
|
expect(amoUploadReady({ processed: false })).toMatchObject({ done: false });
|
|
expect(amoUploadReady({ processed: true, valid: true })).toMatchObject({ done: true, ok: true });
|
|
expect(amoValidationErrors({
|
|
validation: {
|
|
messages: [
|
|
{ type: "error", message: "Missing id" },
|
|
{ type: "warning", message: "innerHTML" },
|
|
],
|
|
},
|
|
})).toEqual(["Missing id"]);
|
|
});
|
|
|
|
test("names the missing credentials without including secret values", () => {
|
|
expect(() => assertCredentials("both", {
|
|
CHROME_WEB_STORE_CLIENT_SECRET: "super-secret",
|
|
})).toThrow("CHROME_WEB_STORE_SERVICE_ACCOUNT_JSON");
|
|
expect(credentialPresence({
|
|
AMO_JWT_ISSUER: "user:1:1",
|
|
AMO_JWT_SECRET: "secret",
|
|
}).amo).toBe(true);
|
|
});
|
|
|
|
test("refuses to submit from a fork", () => {
|
|
expect(() => assertOfficialRepository("someone/edgeever")).toThrow(OFFICIAL_REPOSITORY);
|
|
expect(() => assertOfficialRepository("")).not.toThrow();
|
|
});
|
|
|
|
test("uses a one-line Firefox note when none was provided", () => {
|
|
expect(resolveReleaseNotes({ version: "0.1.9" })).toBe("EdgeEver Web Clipper 0.1.9.");
|
|
});
|
|
|
|
test("includes the review login only in the private approval note", () => {
|
|
const notes = buildApprovalNotes({
|
|
version: "0.1.9",
|
|
reviewInstanceUrl: "https://review.example.test",
|
|
reviewApiToken: "review-token",
|
|
});
|
|
|
|
expect(notes).toContain("https://review.example.test");
|
|
expect(notes).toContain("review-token");
|
|
expect(notes).toContain("bun run package:extension:firefox");
|
|
expect(buildApprovalNotes({ version: "0.1.9" })).not.toContain("Review API token");
|
|
});
|
|
|
|
test("keeps built store packages and dependency installs out of the source archive", () => {
|
|
expect(shouldCopyExtensionSource("src/background.ts")).toBe(true);
|
|
expect(shouldCopyExtensionSource("dist/manifest.json")).toBe(false);
|
|
expect(shouldCopyExtensionSource("node_modules/web-ext/package.json")).toBe(false);
|
|
expect(shouldCopyExtensionSource("store-assets/edgeever-web-clipper-v0.1.8.zip")).toBe(false);
|
|
expect(shouldCopyExtensionSource("store-assets/store-icon-128.png")).toBe(true);
|
|
|
|
const manifests = workspacePackageManifests(new URL("..", import.meta.url).pathname);
|
|
expect(manifests).toContain("apps/web/package.json");
|
|
expect(manifests).toContain("packages/shared/package.json");
|
|
expect(manifests).not.toContain("apps/extension/package.json");
|
|
});
|
|
|
|
test("signs Firefox and Chrome credentials", () => {
|
|
const amo = createAmoJwt({
|
|
issuer: "user:1:1",
|
|
secret: "amo-secret",
|
|
now: 1_700_000_000_000,
|
|
jti: "fixed-jti",
|
|
});
|
|
const [header, payload, signature] = amo.split(".");
|
|
const expected = createHmac("sha256", "amo-secret")
|
|
.update(`${header}.${payload}`)
|
|
.digest("base64url");
|
|
expect(signature).toBe(expected);
|
|
expect(JSON.parse(Buffer.from(payload, "base64url").toString())).toMatchObject({
|
|
iss: "user:1:1",
|
|
jti: "fixed-jti",
|
|
exp: 1_700_000_240,
|
|
});
|
|
|
|
const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
|
|
const assertion = createGoogleServiceAccountAssertion({
|
|
clientEmail: "clipper@example.iam.gserviceaccount.com",
|
|
privateKey: privateKey.export({ type: "pkcs8", format: "pem" }),
|
|
now: 1_700_000_000_000,
|
|
});
|
|
const [googleHeader, googlePayload, googleSignature] = assertion.split(".");
|
|
const verifier = createVerify("RSA-SHA256");
|
|
verifier.update(`${googleHeader}.${googlePayload}`);
|
|
expect(verifier.verify(publicKey, googleSignature, "base64url")).toBe(true);
|
|
expect(JSON.parse(Buffer.from(googlePayload, "base64url").toString()).scope).toContain(
|
|
"chromewebstore",
|
|
);
|
|
});
|
|
|
|
test("publishes Chrome and leaves Firefox compatibility to the manifest", async () => {
|
|
const calls = [];
|
|
const fetchImpl = async (url, options = {}) => {
|
|
calls.push({ url, body: options.body, method: options.method });
|
|
if (String(url).endsWith(":publish")) {
|
|
return jsonResponse({ state: "PENDING_REVIEW" });
|
|
}
|
|
if (String(url).endsWith("/versions/")) {
|
|
return jsonResponse({ version: "0.1.9" });
|
|
}
|
|
return jsonResponse({});
|
|
};
|
|
const chrome = createChromeClient({ fetchImpl, accessToken: "token" });
|
|
await chrome.publish();
|
|
|
|
const directory = mkdtempSync(join(tmpdir(), "edgeever-clipper-api-"));
|
|
const packagePath = join(directory, "addon.zip");
|
|
writeFileSync(packagePath, "zip");
|
|
const firefox = createFirefoxClient({
|
|
fetchImpl,
|
|
issuer: "user:1:1",
|
|
secret: "amo-secret",
|
|
});
|
|
await firefox.uploadPackage(packagePath);
|
|
await firefox.createVersion({
|
|
uploadUuid: "upload-1",
|
|
releaseNotes: "Notes",
|
|
approvalNotes: "Build it",
|
|
});
|
|
await firefox.attachSource({
|
|
version: "0.1.9",
|
|
sourcePath: packagePath,
|
|
releaseNotes: "Notes",
|
|
approvalNotes: "Build it",
|
|
});
|
|
|
|
const publish = calls.find((call) => String(call.url).endsWith(":publish"));
|
|
expect(JSON.parse(publish.body).publishType).toBe("DEFAULT_PUBLISH");
|
|
const created = calls.find((call) => String(call.url).endsWith("/versions/"));
|
|
expect(JSON.parse(created.body)).toMatchObject({
|
|
upload: "upload-1",
|
|
license: FIREFOX_LICENSE_SLUG,
|
|
release_notes: { "en-US": "Notes" },
|
|
});
|
|
expect(JSON.parse(created.body)).not.toHaveProperty("compatibility");
|
|
const edited = calls.find((call) => String(call.url).endsWith("/versions/0.1.9/") && typeof call.body === "string");
|
|
expect(JSON.parse(edited.body)).not.toHaveProperty("compatibility");
|
|
const upload = calls.find((call) => String(call.url).endsWith("/upload/"));
|
|
expect(upload.method).toBe("POST");
|
|
expect(upload.body).toBeInstanceOf(FormData);
|
|
expect(upload.body.get("channel")).toBe("listed");
|
|
});
|
|
|
|
test("keeps store submission on the official repository workflow", () => {
|
|
expect(workflow).toContain("github.repository == 'tianma-if/edgeever'");
|
|
expect(workflow).toContain("workflow_dispatch:");
|
|
expect(workflow).toContain("CHROME_WEB_STORE_SERVICE_ACCOUNT_JSON");
|
|
expect(workflow).toContain("AMO_JWT_ISSUER");
|
|
expect(workflow).toContain("AMO_JWT_SECRET");
|
|
expect(workflow).toContain("edgeever-web-clipper-store");
|
|
expect(workflow).not.toContain("bun run release");
|
|
expect(workflow).not.toContain("publish:stores");
|
|
expect(workflow).not.toContain("echo ${{ secrets");
|
|
});
|
|
});
|
|
|
|
const jsonResponse = (payload) => ({
|
|
ok: true,
|
|
json: async () => payload,
|
|
text: async () => JSON.stringify(payload),
|
|
});
|