mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Parse mod versions leniently like the game does
The mod portal "ApiVersion" enum was validated strictly when loading a mod's info.json, which rejected versions the game itself accepts. Versions are now normalised the same lenient way the game reads them: leading whitespace is allowed, the major and minor are required, the patch is optional, any trailing content is ignored, and numbers are read as integers. This applies to both a mod's version and its factorio_version, validated by a regex schema rather than the fixed enum. The enum is no longer used to validate versions at all - the mod portal is the source of truth, so its error is now surfaced when a version is unsupported. The known-version list kept for the web interface fallback gains 2.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ce8bddad8f
commit
97a6c32838
10 files changed
+141
-81
No files matched your search
@@ -108,9 +108,13 @@ modCommands.add(new lib.Command({
|
||||
},
|
||||
control: Control
|
||||
) {
|
||||
const searchVersion = lib.normaliseGameVersion(String(args.factorioVersion));
|
||||
if (searchVersion === undefined) {
|
||||
throw new Error(`Invalid factorio version "${args.factorioVersion}"`);
|
||||
}
|
||||
let response = await control.send(new lib.ModSearchRequest(
|
||||
args.query,
|
||||
lib.normaliseApiVersion(args.factorioVersion as any),
|
||||
lib.majorMinorVersion(searchVersion),
|
||||
args.page,
|
||||
args.pageSize,
|
||||
args.sort,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Static } from "@sinclair/typebox";
|
||||
import { logger } from "./logging";
|
||||
import { downloadFile, safeOutputFile } from "./file_ops";
|
||||
import { ModPortalReleaseSchema, ModPortalDetailsSchema, ModNameVersionPair } from "./data/messages_mod";
|
||||
import { ModInfo, ModPack, FullVersion, ApiVersion, ModVersionEquality } from "./data";
|
||||
import { ModInfo, ModPack, FullVersion, PartialVersion, majorMinorVersion, ModVersionEquality } from "./data";
|
||||
|
||||
export interface ModStoreEvents {
|
||||
/** A stored mod was created, updated or deleted */
|
||||
@@ -198,18 +198,29 @@ export default class ModStore extends events.EventEmitter<ModStoreEvents> {
|
||||
private async downloadModsChunk(
|
||||
mods: ModNameVersionPair[],
|
||||
username: string, token: string,
|
||||
factorioVersion: ApiVersion,
|
||||
factorioVersion: PartialVersion,
|
||||
) {
|
||||
const url = new URL("https://mods.factorio.com/api/mods");
|
||||
url.searchParams.set("page_size", "max");
|
||||
url.searchParams.set("version", factorioVersion);
|
||||
url.searchParams.set("version", majorMinorVersion(factorioVersion));
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({ namelist: mods.map(m => m.name).join(",") }),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw Error(`Fetch: ${url} returned ${response.status} ${response.statusText}`);
|
||||
// Surface the portal's own error (e.g. an unsupported version) rather
|
||||
// than a generic status line.
|
||||
let detail = `${response.status} ${response.statusText}`;
|
||||
try {
|
||||
const body = await response.json() as { message?: string };
|
||||
if (body && typeof body.message === "string") {
|
||||
detail = body.message;
|
||||
}
|
||||
} catch {
|
||||
// Response body was not JSON; keep the status line.
|
||||
}
|
||||
throw new Error(`Mod portal request to ${url} failed: ${detail}`);
|
||||
}
|
||||
|
||||
const modReleases = (await response.json() as ModsInfoResponse).results;
|
||||
@@ -236,7 +247,7 @@ export default class ModStore extends events.EventEmitter<ModStoreEvents> {
|
||||
* @param factorioVersion - Factorio version to filter for (mods are not guaranteed to work between Middle versions)
|
||||
*/
|
||||
async downloadMods(
|
||||
mods: ModNameVersionPair[], username: string, token: string, factorioVersion: ApiVersion = "1.1"
|
||||
mods: ModNameVersionPair[], username: string, token: string, factorioVersion: PartialVersion = "1.1"
|
||||
) {
|
||||
const chunkSize = 500;
|
||||
const futures: Promise<void>[] = [];
|
||||
|
||||
@@ -8,10 +8,10 @@ import { findRoot } from "../zip_ops";
|
||||
import { ModRecord } from "./ModPack";
|
||||
|
||||
import {
|
||||
ApiVersion,
|
||||
ApiVersionSchema,
|
||||
FullVersion, FullVersionSchema, integerFullVersion,
|
||||
FullVersion, integerFullVersion,
|
||||
integerPartialVersion,
|
||||
GameVersionSchema, normaliseGameVersion, majorMinorVersion,
|
||||
PartialVersion,
|
||||
ModVersionEquality,
|
||||
} from "./version";
|
||||
|
||||
@@ -175,7 +175,7 @@ export default class ModInfo {
|
||||
* Major version of Factorio this mod supports.
|
||||
* Sourced from info.json.
|
||||
*/
|
||||
factorioVersion = "0.12" as "0.12" | ApiVersion;
|
||||
factorioVersion = "0.12" as PartialVersion;
|
||||
|
||||
/**
|
||||
* Integer representation of the factorioVersion
|
||||
@@ -248,16 +248,17 @@ export default class ModInfo {
|
||||
*/
|
||||
isDeleted = false;
|
||||
|
||||
// Content of info.json found in mod files
|
||||
// Content of info.json found in mod files. The version fields use the same
|
||||
// lenient format the game accepts; they are normalised when read in fromJSON.
|
||||
static infoJsonSchema = Type.Object({
|
||||
"name": Type.String(),
|
||||
"version": FullVersionSchema,
|
||||
"version": GameVersionSchema,
|
||||
"title": Type.String(),
|
||||
"author": Type.String(),
|
||||
"contact": Type.Optional(Type.String()),
|
||||
"homepage": Type.Optional(Type.String()),
|
||||
"description": Type.Optional(Type.String()),
|
||||
"factorio_version": Type.Optional(ApiVersionSchema),
|
||||
"factorio_version": Type.Optional(GameVersionSchema),
|
||||
"dependencies": Type.Optional(Type.Array(Type.String())),
|
||||
});
|
||||
|
||||
@@ -279,13 +280,25 @@ export default class ModInfo {
|
||||
|
||||
// info.json fields
|
||||
if (json.name) { modInfo.name = json.name; }
|
||||
if (json.version) { modInfo.version = json.version; }
|
||||
if (json.version) {
|
||||
const version = normaliseGameVersion(json.version);
|
||||
if (version === undefined) {
|
||||
throw new Error(`Invalid mod version "${json.version}"`);
|
||||
}
|
||||
modInfo.version = version;
|
||||
}
|
||||
if (json.title) { modInfo.title = json.title; }
|
||||
if (json.author) { modInfo.author = json.author; }
|
||||
if (json.contact) { modInfo.contact = json.contact; }
|
||||
if (json.homepage) { modInfo.homepage = json.homepage; }
|
||||
if (json.description) { modInfo.description = json.description; }
|
||||
if (json.factorio_version) { modInfo.factorioVersion = json.factorio_version; }
|
||||
if (json.factorio_version) {
|
||||
const factorioVersion = normaliseGameVersion(json.factorio_version);
|
||||
if (factorioVersion === undefined) {
|
||||
throw new Error(`Invalid factorio_version "${json.factorio_version}"`);
|
||||
}
|
||||
modInfo.factorioVersion = majorMinorVersion(factorioVersion);
|
||||
}
|
||||
|
||||
// Parse the dependencies
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { JsonString, jsonArray } from "./composites";
|
||||
|
||||
import {
|
||||
FullVersion, FullVersionSchema,
|
||||
ApiVersion, ApiVersionSchema, normaliseApiVersion,
|
||||
PartialVersion, PartialVersionSchema,
|
||||
GameVersionSchema,
|
||||
ModVersionEquality,
|
||||
} from "./version";
|
||||
|
||||
@@ -154,7 +155,7 @@ export class ModSearchRequest {
|
||||
|
||||
constructor(
|
||||
public query: string,
|
||||
public factorioVersion: ApiVersion,
|
||||
public factorioVersion: PartialVersion,
|
||||
public page: number,
|
||||
public pageSize?: number,
|
||||
public sort?: string,
|
||||
@@ -163,7 +164,7 @@ export class ModSearchRequest {
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"query": Type.String(),
|
||||
"factorioVersion": ApiVersionSchema,
|
||||
"factorioVersion": PartialVersionSchema,
|
||||
"page": Type.Integer(),
|
||||
"pageSize": Type.Optional(Type.Integer()),
|
||||
"sort": Type.Optional(Type.String()),
|
||||
@@ -207,7 +208,7 @@ export class ModSearchRequest {
|
||||
export const ModPortalReleaseSchema = Type.Object({
|
||||
version: FullVersionSchema,
|
||||
// Match the structure from ModStore's ModRelease/ModDetails
|
||||
info_json: Type.Object({ factorio_version: ApiVersionSchema }),
|
||||
info_json: Type.Object({ factorio_version: GameVersionSchema }),
|
||||
released_at: Type.String(), // ISO 8601 date string
|
||||
download_url: Type.String(),
|
||||
file_name: Type.String(),
|
||||
@@ -236,12 +237,12 @@ export class ModPortalGetAllRequest {
|
||||
static permission = "core.mod.search_portal" as const;
|
||||
|
||||
constructor(
|
||||
public factorioVersion: ApiVersion,
|
||||
public factorioVersion: PartialVersion,
|
||||
public hide_deprecated?: boolean,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"factorioVersion": ApiVersionSchema,
|
||||
"factorioVersion": PartialVersionSchema,
|
||||
"hide_deprecated": Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
@@ -385,12 +386,12 @@ export class ModPortalDownloadRequest {
|
||||
|
||||
constructor(
|
||||
public mods: ModNameVersionPair[],
|
||||
public factorioVersion: ApiVersion,
|
||||
public factorioVersion: PartialVersion,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"mods": Type.Array(ModNameVersionPairSchema),
|
||||
"factorioVersion": ApiVersionSchema,
|
||||
"factorioVersion": PartialVersionSchema,
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
@@ -422,13 +423,13 @@ export class ModDependencyResolveRequest {
|
||||
|
||||
constructor(
|
||||
public mods: ModDependency[],
|
||||
public factorioVersion: ApiVersion,
|
||||
public factorioVersion: PartialVersion,
|
||||
public checkForUpdates: boolean = false,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"mods": Type.Array(ModDependency.jsonSchema),
|
||||
"factorioVersion": ApiVersionSchema,
|
||||
"factorioVersion": PartialVersionSchema,
|
||||
"checkForUpdates": Type.Boolean(),
|
||||
});
|
||||
|
||||
@@ -441,7 +442,7 @@ export class ModDependencyResolveRequest {
|
||||
return new this(
|
||||
[...modPack.mods.values()]
|
||||
.map(mod => new ModDependency(`${mod.name} ${equality} ${mod.version}`)),
|
||||
normaliseApiVersion(modPack.factorioVersion),
|
||||
modPack.factorioVersion,
|
||||
checkForUpdates,
|
||||
);
|
||||
}
|
||||
@@ -452,7 +453,7 @@ export class ModDependencyResolveRequest {
|
||||
[...modPack.mods.values()]
|
||||
.filter(mod => mod.enabled)
|
||||
.map(mod => new ModDependency(`${mod.name} ${equality} ${mod.version}`)),
|
||||
normaliseApiVersion(modPack.factorioVersion),
|
||||
modPack.factorioVersion,
|
||||
checkForUpdates,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,25 +24,11 @@ export function isVersionEquality(input: string): input is VersionEquality {
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches valid factorio versions accepts by the web API
|
||||
* https://wiki.factorio.com/Mod_portal_API#/api/mods (see version enum)
|
||||
* Known Factorio major versions, offered as fallback options in the web
|
||||
* interface before the live version list has been fetched. Versions are no
|
||||
* longer validated against this list; the mod portal is the source of truth.
|
||||
*/
|
||||
export const ApiVersions = ["0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "1.0", "1.1", "2.0"] as const;
|
||||
export const ApiVersionSchema = Type.Union(ApiVersions.map(v => Type.Literal(v)));
|
||||
export type ApiVersion = Static<typeof ApiVersionSchema>;
|
||||
|
||||
export function isApiVersion(input: string): input is ApiVersion {
|
||||
return ApiVersions.includes(input as any);
|
||||
}
|
||||
|
||||
export function normaliseApiVersion(version: PartialVersion) {
|
||||
const parts = version.split(".");
|
||||
const apiVersion = `${parts[0]}.${parts[1]}`;
|
||||
if (!isApiVersion(apiVersion)) {
|
||||
throw new Error(`Factorio version not supported by mod portal: ${apiVersion}`);
|
||||
}
|
||||
return apiVersion;
|
||||
}
|
||||
export const ApiVersions = ["0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "1.0", "1.1", "2.0", "2.1"] as const;
|
||||
|
||||
/**
|
||||
* Matches valid mod versions, where all parts are specified.
|
||||
@@ -68,6 +54,31 @@ export function normaliseFullVersion(version: PartialVersion) {
|
||||
return (parts.length === 3 ? version : `${version}.0`) as FullVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a version string the same lenient way the game reads it: each number
|
||||
* may be preceded by whitespace, the major and minor are required, the patch is
|
||||
* optional, and any trailing content is ignored. The pattern is intentionally
|
||||
* not anchored at the end.
|
||||
*/
|
||||
const gameVersionRegExp = /^\s*\d+\.\s*\d+(?:\.\s*\d+)?/;
|
||||
export const GameVersionSchema = Type.String({ pattern: gameVersionRegExp.source });
|
||||
|
||||
/**
|
||||
* Normalise a version string the same lenient way the game reads it. Numbers
|
||||
* are read as integers, so leading whitespace and zeros are dropped and any
|
||||
* content after the patch is ignored.
|
||||
*
|
||||
* @returns The normalised full version, or undefined if no major and minor
|
||||
* could be read.
|
||||
*/
|
||||
export function normaliseGameVersion(input: string): FullVersion | undefined {
|
||||
const match = /^\s*(\d+)\.\s*(\d+)(?:\.\s*(\d+))?/.exec(input);
|
||||
if (match === null) {
|
||||
return undefined;
|
||||
}
|
||||
return `${Number(match[1])}.${Number(match[2])}.${Number(match[3] ?? "0")}` as FullVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches valid factorio versions and mod dependencies specifications where 2 or 3 parts are specified.
|
||||
*/
|
||||
@@ -87,6 +98,14 @@ export function integerPartialVersion(version: PartialVersion) {
|
||||
return major * 0x100000000 + minor * 0x10000 + (sub || 0) as IntegerVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* The major.minor of a version, as used by the mod portal's version filter.
|
||||
*/
|
||||
export function majorMinorVersion(version: PartialVersion) {
|
||||
const [major, minor] = version.split(".");
|
||||
return `${major}.${minor}` as `${number}.${number}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches valid factorio target versions, this is the same as partial but also accepts "latest".
|
||||
*/
|
||||
|
||||
@@ -77,12 +77,12 @@ function SearchModsTable(props: SearchModsTableProps) {
|
||||
const [modResultPageSize, setModResultPageSize] = useState<number>(10);
|
||||
const [modResultCount, setModResultCount] = useState<number>(2);
|
||||
const [modResultSelectedVersion, setModResultSelectedVersion] = useState<Map<string, number>>(new Map());
|
||||
const [factorioVersion, setFactorioVersion] = useState<lib.ApiVersion | null>(null);
|
||||
const [factorioVersion, setFactorioVersion] = useState<lib.PartialVersion | null>(null);
|
||||
|
||||
// Get a valid factorio version
|
||||
useEffect(() => {
|
||||
try {
|
||||
setFactorioVersion(lib.normaliseApiVersion(props.modPack.factorioVersion));
|
||||
setFactorioVersion(lib.majorMinorVersion(props.modPack.factorioVersion));
|
||||
} catch (err) {
|
||||
setFactorioVersion(null);
|
||||
}
|
||||
@@ -271,7 +271,7 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [factorioVersion, setFactorioVersion] = useState<lib.ApiVersion | null>(null);
|
||||
const [factorioVersion, setFactorioVersion] = useState<lib.PartialVersion | null>(null);
|
||||
|
||||
const [mods, setMods] = useState<lib.ModInfo[]>([]); // All resolved mods and dependencies
|
||||
const [missing, setMissing] = useState<lib.ModInfo[]>([]); // Missing mods and dependencies, excluding builtin
|
||||
@@ -295,7 +295,7 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
|
||||
// Get a valid factorio version
|
||||
useEffect(() => {
|
||||
try {
|
||||
setFactorioVersion(lib.normaliseApiVersion(props.modPack.factorioVersion));
|
||||
setFactorioVersion(lib.majorMinorVersion(props.modPack.factorioVersion));
|
||||
} catch (err) {
|
||||
setError(new Error(`Invalid factorio version: ${props.modPack.factorioVersion}`));
|
||||
setFactorioVersion(null);
|
||||
@@ -577,8 +577,8 @@ function ModsTable(props: ModsTableProps) {
|
||||
if (mod.enabled && mod.info) {
|
||||
mod.warning = mod.info.checkDependencySatisfaction(mods.filter(m => m.enabled));
|
||||
try {
|
||||
const packFactorioVersion = lib.normaliseApiVersion(props.modPack.factorioVersion);
|
||||
const modFactorioVersion = lib.normaliseApiVersion(mod.info.factorioVersion);
|
||||
const packFactorioVersion = lib.majorMinorVersion(props.modPack.factorioVersion);
|
||||
const modFactorioVersion = lib.majorMinorVersion(mod.info.factorioVersion);
|
||||
if (packFactorioVersion !== modFactorioVersion) {
|
||||
mod.warning = "wrong_factorio_version";
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ function SearchModsButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [factorioVersion, setFactorioVersion] = useState<lib.ApiVersion>("2.0");
|
||||
const [factorioVersion, setFactorioVersion] = useState<lib.PartialVersion>("2.0");
|
||||
|
||||
// State for all mods fetched from backend
|
||||
const [allMods, setAllMods] = useState<ModPortalModType[]>([]);
|
||||
@@ -165,7 +165,7 @@ function SearchModsButton() {
|
||||
modName: string,
|
||||
modTitle: string | undefined,
|
||||
modVersion: lib.FullVersion | undefined,
|
||||
portalFactorioVersion: lib.ApiVersion,
|
||||
portalFactorioVersion: lib.PartialVersion,
|
||||
) => {
|
||||
if (modVersion) {
|
||||
control.send(
|
||||
@@ -271,7 +271,7 @@ function SearchModsButton() {
|
||||
// Update search text and factorio version from form
|
||||
const handleSearch = (changedValues: any, allValues: any) => {
|
||||
const nameValue = allValues.name;
|
||||
const versionValue = allValues.factorioVersion as lib.ApiVersion | undefined;
|
||||
const versionValue = allValues.factorioVersion as lib.PartialVersion | undefined;
|
||||
|
||||
// Only trigger state updates if values actually changed
|
||||
if (nameValue !== searchText) {
|
||||
|
||||
@@ -358,7 +358,7 @@ describe("lib/ModStore", function () {
|
||||
|
||||
await assert.rejects(
|
||||
modStore.downloadMods(modsToDownload, username, token, factorioVersion),
|
||||
/Fetch: https:\/\/mods\.factorio\.com\/api\/mods.* returned 503 Service Unavailable/
|
||||
/Mod portal request to https:\/\/mods\.factorio\.com\/api\/mods.* failed: 503 Service Unavailable/
|
||||
);
|
||||
assert.equal(modStore.files.size, 0, "No mods should be added on API error");
|
||||
});
|
||||
|
||||
@@ -341,6 +341,21 @@ describe("lib/data/ModInfo", function() {
|
||||
dependencies: ["base", ""],
|
||||
}), /my-mod/); // check the mod name is included in the error message
|
||||
});
|
||||
it("should normalise lenient version fields the way the game reads them", function() {
|
||||
const mod = ModInfo.fromJSON({
|
||||
name: "my-mod",
|
||||
version: " 1.2.3-extra",
|
||||
factorio_version: "1.1.0",
|
||||
});
|
||||
assert.equal(mod.version, "1.2.3");
|
||||
assert.equal(mod.factorioVersion, "1.1");
|
||||
});
|
||||
it("should throw for an unparseable mod version", function() {
|
||||
assert.throws(
|
||||
() => ModInfo.fromJSON({ name: "my-mod", version: "not-a-version" }),
|
||||
/Invalid mod version/
|
||||
);
|
||||
});
|
||||
it("should sort integer mod versions lexicographically", function() {
|
||||
let unsortedVersions = ["1.0.0", "1.1.0", "0.1.0", "3.0.0", "1.2.0", "0.3.1", "0.3.3", "2.1.1", "0.0.1"];
|
||||
let sortedVersions = ["0.0.1", "0.1.0", "0.3.1", "0.3.3", "1.0.0", "1.1.0", "1.2.0", "2.1.1", "3.0.0"];
|
||||
|
||||
@@ -17,36 +17,33 @@ describe("lib/data/version", function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("isApiVersion()", function() {
|
||||
it("should correctly validate input strings", function() {
|
||||
const valid = ["0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "1.0", "1.1", "2.0"];
|
||||
for (const test of valid) {
|
||||
assert.equal(lib.isApiVersion(test), true, test);
|
||||
}
|
||||
const invalid = ["0.12", "0.19", "1.2", "2.1", "1.0.0", "1.0.0.0", "0", "", "latest"];
|
||||
describe("normaliseGameVersion()", function() {
|
||||
it("should normalise lenient version strings the way the game reads them", function() {
|
||||
// Major and minor required, patch optional and defaulting to 0.
|
||||
assert.equal(lib.normaliseGameVersion("1.1"), "1.1.0");
|
||||
assert.equal(lib.normaliseGameVersion("1.1.50"), "1.1.50");
|
||||
assert.equal(lib.normaliseGameVersion("0.18"), "0.18.0");
|
||||
// Leading whitespace and zeros are dropped.
|
||||
assert.equal(lib.normaliseGameVersion(" 1.1"), "1.1.0");
|
||||
assert.equal(lib.normaliseGameVersion("01.02.03"), "1.2.3");
|
||||
assert.equal(lib.normaliseGameVersion("1. 1. 50"), "1.1.50");
|
||||
// Anything after the patch (or a non-numeric patch) is ignored.
|
||||
assert.equal(lib.normaliseGameVersion("1.1.50-beta"), "1.1.50");
|
||||
assert.equal(lib.normaliseGameVersion("1.1.x"), "1.1.0");
|
||||
assert.equal(lib.normaliseGameVersion("1.1.50.60"), "1.1.50");
|
||||
});
|
||||
it("should return undefined when no major.minor can be read", function() {
|
||||
const invalid = ["1", "", "latest", "abc", ".1.1", "x.y"];
|
||||
for (const test of invalid) {
|
||||
assert.equal(lib.isApiVersion(test), false, test);
|
||||
assert.equal(lib.normaliseGameVersion(test), undefined, test);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("normaliseApiVersion()", function() {
|
||||
it("should normalise accepted versions", function() {
|
||||
assert.equal(lib.normaliseApiVersion("0.13"), "0.13");
|
||||
assert.equal(lib.normaliseApiVersion("0.13.0"), "0.13");
|
||||
assert.equal(lib.normaliseApiVersion("0.13.1"), "0.13");
|
||||
assert.equal(lib.normaliseApiVersion("1.1"), "1.1");
|
||||
assert.equal(lib.normaliseApiVersion("1.1.0"), "1.1");
|
||||
assert.equal(lib.normaliseApiVersion("1.1.1"), "1.1");
|
||||
assert.equal(lib.normaliseApiVersion("2.0"), "2.0");
|
||||
assert.equal(lib.normaliseApiVersion("2.0.0"), "2.0");
|
||||
assert.equal(lib.normaliseApiVersion("2.0.1"), "2.0");
|
||||
assert.equal(lib.normaliseApiVersion("1.0.0.0"), "1.0");
|
||||
});
|
||||
it("should throw for invalid versions", function() {
|
||||
const invalid = ["0.12", "0", "", "latest"];
|
||||
for (const test of invalid) {
|
||||
assert.throws(() => lib.normaliseApiVersion(test), undefined, test);
|
||||
}
|
||||
describe("majorMinorVersion()", function() {
|
||||
it("should return the major.minor", function() {
|
||||
assert.equal(lib.majorMinorVersion("1.1"), "1.1");
|
||||
assert.equal(lib.majorMinorVersion("1.1.50"), "1.1");
|
||||
assert.equal(lib.majorMinorVersion("0.18.0"), "0.18");
|
||||
});
|
||||
});
|
||||
describe("isFullVersion()", function() {
|
||||
|
||||
Reference in new issue
Block a user