mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-08-24 02:57:43 +02:00
website: prevent incomplete version-info deployments and regenerate validated update feeds after releases
This commit is contained in:
@@ -4,7 +4,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "eleventy --serve",
|
"start": "eleventy --serve",
|
||||||
"build": "eleventy"
|
"build": "eleventy && npm run validate:version-info",
|
||||||
|
"validate:version-info": "node scripts/validate-version-info.js",
|
||||||
|
"test": "node --test test/*.test.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@11ty/eleventy": "^3.0.0"
|
"@11ty/eleventy": "^3.0.0"
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const DEFAULT_FILE = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"_site",
|
||||||
|
"kst4ContestVersionInfo.xml"
|
||||||
|
);
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
throw new Error(`[versionInfo validation] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normaliseVersion(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.replace(/^v/, "")
|
||||||
|
.split("-")[0]
|
||||||
|
.split("+")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTag(xml, tagName) {
|
||||||
|
const match = xml.match(
|
||||||
|
new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`)
|
||||||
|
);
|
||||||
|
|
||||||
|
return match ? match[1].trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertWellFormedStructure(xml) {
|
||||||
|
if (/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-f]+;)/i.test(xml)) {
|
||||||
|
fail("the XML contains an unescaped ampersand");
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutCommentsAndDeclaration = xml
|
||||||
|
.replace(/<!--[\s\S]*?-->/g, "")
|
||||||
|
.replace(/<\?[\s\S]*?\?>/g, "");
|
||||||
|
|
||||||
|
const tagPattern =
|
||||||
|
/<\/?([A-Za-z_][\w:.-]*)(?:\s[^<>]*?)?\s*\/?>/g;
|
||||||
|
|
||||||
|
const stack = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while (
|
||||||
|
(match = tagPattern.exec(withoutCommentsAndDeclaration)) !== null
|
||||||
|
) {
|
||||||
|
const fullTag = match[0];
|
||||||
|
const tagName = match[1];
|
||||||
|
|
||||||
|
if (fullTag.startsWith("</")) {
|
||||||
|
const openTag = stack.pop();
|
||||||
|
|
||||||
|
if (openTag !== tagName) {
|
||||||
|
fail(
|
||||||
|
`closing tag </${tagName}> does not match `
|
||||||
|
+ `<${openTag || "none"}>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (!fullTag.endsWith("/>")) {
|
||||||
|
stack.push(tagName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack.length > 0) {
|
||||||
|
fail(`unclosed tag <${stack[stack.length - 1]}>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingMarkup =
|
||||||
|
withoutCommentsAndDeclaration.replace(tagPattern, "");
|
||||||
|
|
||||||
|
// A plain ">" is legal character data and occurs in historical
|
||||||
|
// changelog notation such as "->". A remaining "<" cannot be legal
|
||||||
|
// after all valid tags have been removed.
|
||||||
|
if (/</.test(remainingMarkup)) {
|
||||||
|
fail("the XML contains malformed markup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertContainsTag(block, tagName) {
|
||||||
|
const pattern =
|
||||||
|
new RegExp(`<${tagName}>[\\s\\S]*?<\\/${tagName}>`);
|
||||||
|
|
||||||
|
if (!pattern.test(block)) {
|
||||||
|
fail(`required element <${tagName}> is missing`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateVersionInfo(xml, expectedStableVersion = "") {
|
||||||
|
if (!xml || Buffer.byteLength(xml, "utf8") < 500) {
|
||||||
|
fail("the generated file is empty or implausibly small");
|
||||||
|
}
|
||||||
|
|
||||||
|
assertWellFormedStructure(xml);
|
||||||
|
|
||||||
|
const completeDocumentPattern =
|
||||||
|
/^<\?xml[^>]*>\s*<praktiKST>[\s\S]*<\/praktiKST>\s*$/;
|
||||||
|
|
||||||
|
if (!completeDocumentPattern.test(xml)) {
|
||||||
|
fail(
|
||||||
|
"the document does not contain one complete "
|
||||||
|
+ "<praktiKST> root element"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersionBlock = extractTag(xml, "latestVersion");
|
||||||
|
|
||||||
|
if (latestVersionBlock === null) {
|
||||||
|
fail("<latestVersion> is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tagName of [
|
||||||
|
"versionNumber",
|
||||||
|
"semanticVersion",
|
||||||
|
"adminMessage",
|
||||||
|
"majorChanges",
|
||||||
|
"latestVersionPathOnWebserver"
|
||||||
|
]) {
|
||||||
|
assertContainsTag(latestVersionBlock, tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyVersion =
|
||||||
|
extractTag(latestVersionBlock, "versionNumber");
|
||||||
|
|
||||||
|
const semanticVersion =
|
||||||
|
extractTag(latestVersionBlock, "semanticVersion");
|
||||||
|
|
||||||
|
const releaseUrl =
|
||||||
|
extractTag(
|
||||||
|
latestVersionBlock,
|
||||||
|
"latestVersionPathOnWebserver"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!legacyVersion
|
||||||
|
|| !/^\d+(?:\.\d+)?$/.test(legacyVersion)
|
||||||
|
) {
|
||||||
|
fail(
|
||||||
|
"<versionNumber> is not a valid legacy numeric version"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!semanticVersion
|
||||||
|
|| !/^\d+\.\d+(?:\.\d+)?$/.test(semanticVersion)
|
||||||
|
) {
|
||||||
|
fail("<semanticVersion> is not a valid Stable version");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!releaseUrl
|
||||||
|
|| !releaseUrl.startsWith(
|
||||||
|
"https://github.com/praktimarc/"
|
||||||
|
+ "kst4contest/releases/tag/"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
fail(
|
||||||
|
"<latestVersionPathOnWebserver> is not "
|
||||||
|
+ "a KST4Contest release URL"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeLogs = [
|
||||||
|
...xml.matchAll(
|
||||||
|
/<changeLog>([\s\S]*?)<\/changeLog>/g
|
||||||
|
)
|
||||||
|
].map((match) => match[1]);
|
||||||
|
|
||||||
|
if (changeLogs.length === 0) {
|
||||||
|
fail(
|
||||||
|
"the document does not contain any "
|
||||||
|
+ "<changeLog> entries"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of changeLogs) {
|
||||||
|
for (const tagName of [
|
||||||
|
"changedVersionNumber",
|
||||||
|
"date",
|
||||||
|
"description",
|
||||||
|
"added",
|
||||||
|
"changed",
|
||||||
|
"fixed",
|
||||||
|
"removed"
|
||||||
|
]) {
|
||||||
|
assertContainsTag(entry, tagName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected = normaliseVersion(expectedStableVersion);
|
||||||
|
|
||||||
|
if (expected) {
|
||||||
|
if (semanticVersion !== expected) {
|
||||||
|
fail(
|
||||||
|
`latest Stable version ${semanticVersion} `
|
||||||
|
+ `does not match expected release ${expected}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseEntryExists = changeLogs.some(
|
||||||
|
(entry) =>
|
||||||
|
extractTag(
|
||||||
|
entry,
|
||||||
|
"changedVersionNumber"
|
||||||
|
) === expected
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!releaseEntryExists) {
|
||||||
|
fail(
|
||||||
|
"the changelog does not contain the expected "
|
||||||
|
+ `Stable release ${expected}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
semanticVersion,
|
||||||
|
changeLogEntries: changeLogs.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArguments(argv) {
|
||||||
|
const result = {
|
||||||
|
file: DEFAULT_FILE,
|
||||||
|
expectedStableVersion:
|
||||||
|
process.env.EXPECTED_STABLE_VERSION || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < argv.length; index++) {
|
||||||
|
if (
|
||||||
|
argv[index] === "--file"
|
||||||
|
&& argv[index + 1]
|
||||||
|
) {
|
||||||
|
result.file = path.resolve(argv[++index]);
|
||||||
|
} else if (
|
||||||
|
argv[index] === "--expected-stable"
|
||||||
|
&& argv[index + 1]
|
||||||
|
) {
|
||||||
|
result.expectedStableVersion = argv[++index];
|
||||||
|
} else {
|
||||||
|
fail(
|
||||||
|
`unknown or incomplete argument: ${argv[index]}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
try {
|
||||||
|
const options =
|
||||||
|
parseArguments(process.argv.slice(2));
|
||||||
|
|
||||||
|
const xml =
|
||||||
|
fs.readFileSync(options.file, "utf8");
|
||||||
|
|
||||||
|
const result =
|
||||||
|
validateVersionInfo(
|
||||||
|
xml,
|
||||||
|
options.expectedStableVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[versionInfo validation] OK: `
|
||||||
|
+ `Stable ${result.semanticVersion}, `
|
||||||
|
+ `${result.changeLogEntries} changelog entries, `
|
||||||
|
+ options.file
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
normaliseVersion,
|
||||||
|
validateVersionInfo
|
||||||
|
};
|
||||||
@@ -5,18 +5,95 @@ const REPO = "praktimarc/kst4contest";
|
|||||||
const API = `https://api.github.com/repos/${REPO}`;
|
const API = `https://api.github.com/repos/${REPO}`;
|
||||||
const LABEL_MAP = { enhancement: "added", bug: "fixed" };
|
const LABEL_MAP = { enhancement: "added", bug: "fixed" };
|
||||||
|
|
||||||
|
const GITHUB_API_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
function wait(milliseconds) {
|
||||||
|
return new Promise(
|
||||||
|
(resolve) => setTimeout(resolve, milliseconds)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function githubGet(urlPath) {
|
async function githubGet(urlPath) {
|
||||||
const headers = { Accept: "application/vnd.github+json" };
|
const headers = {
|
||||||
|
Accept: "application/vnd.github+json"
|
||||||
|
};
|
||||||
|
|
||||||
if (process.env.GITHUB_TOKEN) {
|
if (process.env.GITHUB_TOKEN) {
|
||||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
headers.Authorization =
|
||||||
|
`Bearer ${process.env.GITHUB_TOKEN}`;
|
||||||
}
|
}
|
||||||
const res = await fetch(`${API}${urlPath}`, { headers });
|
|
||||||
if (!res.ok) {
|
let lastError = null;
|
||||||
throw new Error(`GitHub API ${urlPath} failed: ${res.status}`);
|
|
||||||
|
for (
|
||||||
|
let attempt = 1;
|
||||||
|
attempt <= GITHUB_API_ATTEMPTS;
|
||||||
|
attempt++
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${API}${urlPath}`,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
signal: AbortSignal.timeout(15000)
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rateLimitRemaining =
|
||||||
|
res.headers.get("x-ratelimit-remaining");
|
||||||
|
|
||||||
|
const rateLimitReset =
|
||||||
|
res.headers.get("x-ratelimit-reset");
|
||||||
|
|
||||||
|
const rateLimitInfo =
|
||||||
|
rateLimitRemaining === null
|
||||||
|
? ""
|
||||||
|
: `, rate limit remaining `
|
||||||
|
+ rateLimitRemaining
|
||||||
|
+ (
|
||||||
|
rateLimitReset
|
||||||
|
? `, reset ${rateLimitReset}`
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
lastError = new Error(
|
||||||
|
`GitHub API ${urlPath} failed with `
|
||||||
|
+ `HTTP ${res.status}${rateLimitInfo}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Authentication and permission errors do not
|
||||||
|
// become valid by retrying.
|
||||||
|
if (
|
||||||
|
![408, 429].includes(res.status)
|
||||||
|
&& res.status < 500
|
||||||
|
) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
|
||||||
|
// Do not hide an invalid or expired token behind
|
||||||
|
// repeated requests.
|
||||||
|
if (
|
||||||
|
/HTTP (401|403|404)/.test(err.message)
|
||||||
|
) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < GITHUB_API_ATTEMPTS) {
|
||||||
|
await wait(attempt * 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError
|
||||||
|
|| new Error(`GitHub API ${urlPath} failed`);
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateChecker.java parses <versionNumber> with Double.parseDouble() and
|
// UpdateChecker.java parses <versionNumber> with Double.parseDouble() and
|
||||||
// compares it against ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER,
|
// compares it against ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER,
|
||||||
// which encodes patch releases by appending the patch digit(s) instead of a
|
// which encodes patch releases by appending the patch digit(s) instead of a
|
||||||
@@ -108,25 +185,64 @@ function loadLegacySections() {
|
|||||||
// UpdateChecker.java) from GitHub releases + closed issues, falling back to
|
// UpdateChecker.java) from GitHub releases + closed issues, falling back to
|
||||||
// version-history.xml for releases that predate GitHub Releases.
|
// version-history.xml for releases that predate GitHub Releases.
|
||||||
module.exports = async function () {
|
module.exports = async function () {
|
||||||
const fallback = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<praktiKST></praktiKST>';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historyEntries = loadHistoryEntries();
|
const historyEntries = loadHistoryEntries();
|
||||||
|
|
||||||
const rawReleases = await githubGet("/releases?per_page=100");
|
const rawReleases =
|
||||||
|
await githubGet("/releases?per_page=100");
|
||||||
|
|
||||||
|
if (!Array.isArray(rawReleases)) {
|
||||||
|
throw new Error(
|
||||||
|
"GitHub releases response was not an array"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const releases = rawReleases
|
const releases = rawReleases
|
||||||
.map((r) => ({
|
.map((release) => ({
|
||||||
tagName: r.tag_name,
|
tagName: release.tag_name,
|
||||||
publishedAt: r.published_at || "",
|
publishedAt: release.published_at || "",
|
||||||
name: r.name || r.tag_name,
|
name:
|
||||||
body: r.body || "",
|
release.name
|
||||||
isPrerelease: r.prerelease
|
|| release.tag_name,
|
||||||
|
body: release.body || "",
|
||||||
|
isPrerelease: release.prerelease,
|
||||||
|
isDraft: release.draft
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1));
|
.sort(
|
||||||
|
(first, second) =>
|
||||||
|
first.publishedAt
|
||||||
|
< second.publishedAt
|
||||||
|
? 1
|
||||||
|
: -1
|
||||||
|
);
|
||||||
|
|
||||||
|
const stableReleases = releases.filter(
|
||||||
|
(release) =>
|
||||||
|
!release.isPrerelease
|
||||||
|
&& !release.isDraft
|
||||||
|
);
|
||||||
|
|
||||||
const stableReleases = releases.filter((r) => !r.isPrerelease);
|
|
||||||
const stable = stableReleases[0];
|
const stable = stableReleases[0];
|
||||||
const ghVersions = new Set(stableReleases.map((r) => toAppVersionNumber(r.tagName)));
|
|
||||||
|
if (
|
||||||
|
!stable
|
||||||
|
|| !stable.tagName
|
||||||
|
|| !stable.publishedAt
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"GitHub did not return "
|
||||||
|
+ "a published Stable release"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghVersions = new Set(
|
||||||
|
stableReleases.map(
|
||||||
|
(release) =>
|
||||||
|
toAppVersionNumber(
|
||||||
|
release.tagName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
parts.push('<?xml version="1.0" encoding="UTF-8" standalone="no"?>');
|
parts.push('<?xml version="1.0" encoding="UTF-8" standalone="no"?>');
|
||||||
@@ -203,7 +319,14 @@ module.exports = async function () {
|
|||||||
parts.push("</praktiKST>");
|
parts.push("</praktiKST>");
|
||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[versionInfo] Could not generate version info XML, using empty fallback: ${err.message}`);
|
throw new Error(
|
||||||
return fallback;
|
"[versionInfo] Could not generate "
|
||||||
|
+ "a complete update feed. "
|
||||||
|
+ "The website build has been aborted "
|
||||||
|
+ "so that the existing production feed "
|
||||||
|
+ "remains untouched: "
|
||||||
|
+ err.message,
|
||||||
|
{ cause: err }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const test = require("node:test");
|
||||||
|
|
||||||
|
const generateVersionInfo =
|
||||||
|
require("../src/_data/versionInfo");
|
||||||
|
|
||||||
|
const {
|
||||||
|
validateVersionInfo
|
||||||
|
} = require("../scripts/validate-version-info");
|
||||||
|
|
||||||
|
const VALID_XML =
|
||||||
|
`<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<praktiKST>
|
||||||
|
<latestVersion>
|
||||||
|
<versionNumber>1.411</versionNumber>
|
||||||
|
<semanticVersion>1.41.1</semanticVersion>
|
||||||
|
<adminMessage></adminMessage>
|
||||||
|
<majorChanges>Hotfix</majorChanges>
|
||||||
|
<latestVersionPathOnWebserver>https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1</latestVersionPathOnWebserver>
|
||||||
|
</latestVersion>
|
||||||
|
<needUpdateSinceLastVersion>
|
||||||
|
<filename>nothing</filename>
|
||||||
|
</needUpdateSinceLastVersion>
|
||||||
|
<changeLog>
|
||||||
|
<changedVersionNumber>1.41.1</changedVersionNumber>
|
||||||
|
<date>2026-07-08</date>
|
||||||
|
<description>Hotfix</description>
|
||||||
|
<added></added>
|
||||||
|
<changed></changed>
|
||||||
|
<fixed>Text input handling</fixed>
|
||||||
|
<removed></removed>
|
||||||
|
</changeLog>
|
||||||
|
</praktiKST>`;
|
||||||
|
|
||||||
|
test("accepts a complete update feed", () => {
|
||||||
|
const result =
|
||||||
|
validateVersionInfo(VALID_XML, "v1.41.1");
|
||||||
|
|
||||||
|
assert.equal(result.semanticVersion, "1.41.1");
|
||||||
|
assert.equal(result.changeLogEntries, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects the former empty fallback", () => {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
validateVersionInfo(
|
||||||
|
'<?xml version="1.0" '
|
||||||
|
+ 'encoding="UTF-8"?>'
|
||||||
|
+ "<praktiKST></praktiKST>"
|
||||||
|
),
|
||||||
|
/empty or implausibly small/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"rejects a feed which does not contain "
|
||||||
|
+ "the expected Stable release",
|
||||||
|
() => {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
validateVersionInfo(
|
||||||
|
VALID_XML,
|
||||||
|
"v1.42.0"
|
||||||
|
),
|
||||||
|
/does not match expected release/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"aborts generation when GitHub rejects "
|
||||||
|
+ "the API request",
|
||||||
|
async () => {
|
||||||
|
const originalFetch = global.fetch;
|
||||||
|
|
||||||
|
global.fetch = async () => ({
|
||||||
|
ok: false,
|
||||||
|
status: 401,
|
||||||
|
headers: {
|
||||||
|
get: () => null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
generateVersionInfo(),
|
||||||
|
/website build has been aborted.*HTTP 401/i
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
global.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user