CORPORATION: Rewrite validation code for strings of price and quantity (#1753)

This commit is contained in:
catloversg 2024-11-12 22:39:21 +07:00 committed by GitHub
parent 9bba6a0a41
commit 246d668951
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 240 additions and 177 deletions

@ -198,125 +198,89 @@ export function acceptInvestmentOffer(corporation: Corporation): void {
corporation.investorShares += investShares; corporation.investorShares += investShares;
} }
export function convertPriceString(price: string): string {
/**
* Replace invalid characters. Only accepts:
* - Digit characters
* - 4 most basic algebraic operations (+ - * /)
* - Parentheses
* - Dot character
* - Any characters in this list: [e, E, M, P]
*/
const sanitizedPrice = price.replace(/[^\d+\-*/().eEMP]/g, "");
// Replace MP with test numbers.
for (const testNumber of [-1.2e123, -123456, 123456, 1.2e123]) {
const temp = sanitizedPrice.replace(/MP/g, testNumber.toString());
let evaluatedTemp: unknown;
try {
evaluatedTemp = eval?.(temp);
if (typeof evaluatedTemp !== "number" || !Number.isFinite(evaluatedTemp)) {
throw new Error(
`Evaluated value is not a valid number: ${evaluatedTemp}. Price: ${price}. sanitizedPrice: ${sanitizedPrice}. testNumber: ${testNumber}.`,
);
}
} catch (error) {
throw new Error(`Invalid value or expression for sell price field: ${error}`, { cause: error });
}
}
// Use sanitized price.
return sanitizedPrice;
}
export function convertAmountString(amount: string): string {
/**
* Replace invalid characters. Only accepts:
* - Digit characters
* - 4 most basic algebraic operations (+ - * /)
* - Parentheses
* - Dot character
* - Any characters in this list: [e, E, M, A, X, P, R, O, D, I, N, V]
*/
const sanitizedAmount = amount.replace(/[^\d+\-*/().eEMAXPRODINV]/g, "");
for (const testNumber of [-1.2e123, -123456, 123456, 1.2e123]) {
let temp = sanitizedAmount.replace(/MAX/g, testNumber.toString());
temp = temp.replace(/PROD/g, testNumber.toString());
temp = temp.replace(/INV/g, testNumber.toString());
let evaluatedTemp: unknown;
try {
evaluatedTemp = eval?.(temp);
if (typeof evaluatedTemp !== "number" || !Number.isFinite(evaluatedTemp)) {
throw new Error(
`Evaluated value is not a valid number: ${evaluatedTemp}. Amount: ${amount}. sanitizedAmount: ${sanitizedAmount}. testNumber: ${testNumber}.`,
);
}
} catch (error) {
throw new Error(`Invalid value or expression for sell quantity field: ${error}`, { cause: error });
}
}
// Use sanitized amount.
return sanitizedAmount;
}
export function sellMaterial(material: Material, amount: string, price: string): void { export function sellMaterial(material: Material, amount: string, price: string): void {
if (price === "") price = "0"; const convertedPrice = convertPriceString(price.toUpperCase());
if (amount === "") amount = "0"; const convertedAmount = convertAmountString(amount.toUpperCase());
let cost = price.replace(/\s+/g, "");
cost = cost.replace(/[^-()\d/*+.MPe]/g, ""); //Sanitize cost
let temp = cost.replace(/MP/, "1.234e5");
try {
if (temp.includes("MP")) throw "Only one reference to MP is allowed in sell price.";
temp = eval?.(temp);
} catch (error) {
throw new Error("Invalid value or expression for sell price field", { cause: error });
}
if (temp == null || isNaN(parseFloat(temp))) { material.desiredSellPrice = convertedPrice;
throw new Error("Invalid value or expression for sell price field"); material.desiredSellAmount = convertedAmount;
}
if (cost.includes("MP")) {
material.desiredSellPrice = cost; //Dynamically evaluated
} else {
material.desiredSellPrice = temp;
}
//Parse quantity
amount = amount.toUpperCase();
if (amount.includes("MAX") || amount.includes("PROD") || amount.includes("INV")) {
let q = amount.replace(/\s+/g, "");
q = q.replace(/[^-()\d/*+.MAXPRODINV]/g, "");
let tempQty = q.replace(/MAX/g, material.maxSellPerCycle.toString());
tempQty = tempQty.replace(/PROD/g, material.productionAmount.toString());
tempQty = tempQty.replace(/INV/g, material.productionAmount.toString());
try {
tempQty = eval?.(tempQty);
} catch (error) {
throw new Error("Invalid value or expression for sell quantity field", { cause: error });
}
if (tempQty == null || isNaN(parseFloat(tempQty))) {
throw new Error("Invalid value or expression for sell quantity field");
}
material.desiredSellAmount = q; //Use sanitized input
} else if (isNaN(parseFloat(amount)) || parseFloat(amount) < 0) {
throw new Error("Invalid value for sell quantity field! Must be numeric or 'PROD' or 'MAX'");
} else {
let q = parseFloat(amount);
if (isNaN(q)) {
q = 0;
}
material.desiredSellAmount = q;
}
} }
export function sellProduct(product: Product, city: CityName, amt: string, price: string, all: boolean): void { export function sellProduct(product: Product, city: CityName, amt: string, price: string, all: boolean): void {
//Parse price const convertedPrice = convertPriceString(price.toUpperCase());
// initliaze newPrice with oldPrice as default const convertedAmount = convertAmountString(amt.toUpperCase());
let newPrice = product.cityData[city].desiredSellPrice;
if (price.includes("MP")) {
//Dynamically evaluated quantity. First test to make sure its valid
//Sanitize input, then replace dynamic variables with arbitrary numbers
price = price.replace(/\s+/g, "");
price = price.replace(/[^-()\d/*+.MPe]/g, "");
let temp = price.replace(/MP/, "1.234e5");
try {
if (temp.includes("MP")) throw "Only one reference to MP is allowed in sell price.";
temp = eval?.(temp);
} catch (error) {
throw new Error("Invalid value or expression for sell price field.", { cause: error });
}
if (temp == null || isNaN(parseFloat(temp))) {
throw new Error("Invalid value or expression for sell price field.");
}
newPrice = price; //Use sanitized price
} else {
const cost = parseFloat(price);
if (isNaN(cost)) {
throw new Error("Invalid value for sell price field");
}
newPrice = cost;
}
// Parse quantity
amt = amt.toUpperCase();
//initialize newAmount with old as default
let newAmount = product.cityData[city].desiredSellAmount;
if (amt.includes("MAX") || amt.includes("PROD") || amt.includes("INV")) {
//Dynamically evaluated quantity. First test to make sure its valid
let qty = amt.replace(/\s+/g, "");
qty = qty.replace(/[^-()\d/*+.MAXPRODINV]/g, "");
let temp = qty.replace(/MAX/g, product.maxSellAmount.toString());
temp = temp.replace(/PROD/g, product.cityData[city].productionAmount.toString());
temp = temp.replace(/INV/g, product.cityData[city].stored.toString());
try {
temp = eval?.(temp);
} catch (error) {
throw new Error("Invalid value or expression for sell quantity field", { cause: error });
}
if (temp == null || isNaN(parseFloat(temp))) {
throw new Error("Invalid value or expression for sell quantity field");
}
newAmount = qty; //Use sanitized input
} else if (isNaN(parseFloat(amt)) || parseFloat(amt) < 0) {
throw new Error("Invalid value for sell quantity field! Must be numeric or 'PROD' or 'MAX'");
} else {
let qty = parseFloat(amt);
if (isNaN(qty)) {
qty = 0;
}
newAmount = qty;
}
//apply new price and amount to all or just current
if (all) { if (all) {
for (const cityName of Object.values(CityName)) { for (const cityName of Object.values(CityName)) {
product.cityData[cityName].desiredSellAmount = newAmount; product.cityData[cityName].desiredSellAmount = convertedAmount;
product.cityData[cityName].desiredSellPrice = newPrice; product.cityData[cityName].desiredSellPrice = convertedPrice;
} }
} else { } else {
product.cityData[city].desiredSellAmount = newAmount; product.cityData[city].desiredSellAmount = convertedAmount;
product.cityData[city].desiredSellPrice = newPrice; product.cityData[city].desiredSellPrice = convertedPrice;
} }
} }
@ -577,23 +541,21 @@ Attempted export amount: ${amount}`);
sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAXEPRODINV]/g, ""); sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAXEPRODINV]/g, "");
for (const testReplacement of ["(1.23)", "(-1.23)"]) { for (const testReplacement of ["(1.23)", "(-1.23)"]) {
const replaced = sanitizedAmt.replace(/(MAX|IPROD|EPROD|IINV|EINV)/g, testReplacement); const replaced = sanitizedAmt.replace(/(MAX|IPROD|EPROD|IINV|EINV)/g, testReplacement);
let evaluated, error; let evaluated: unknown;
try { try {
evaluated = eval?.(replaced); evaluated = eval?.(replaced);
} catch (e) { if (typeof evaluated !== "number" || !Number.isFinite(evaluated)) {
error = e; throw new Error(`Evaluated value is not a valid number: ${evaluated}`);
} }
if (!error && isNaN(evaluated)) error = "evaluated value is NaN"; } catch (error) {
if (error) {
throw new Error( throw new Error(
`Error while trying to set the exported amount of ${material.name}. `Error while trying to set the exported amount of ${material.name}.
Error occurred while testing keyword replacement with ${testReplacement}. Error occurred while testing keyword replacement with ${testReplacement}.
Your input: ${amount} Your input: ${amount}
Sanitized input: ${sanitizedAmt} Sanitized input: ${sanitizedAmt}
Input after replacement: ${replaced} Input after replacement: ${replaced}
Evaluated value: ${evaluated}` + Evaluated value: ${evaluated}
// eslint-disable-next-line @typescript-eslint/no-base-to-string Error encountered: ${error}`,
`Error encountered: ${error}`,
); );
} }
} }

@ -16,6 +16,7 @@ import { PartialRecord, getRecordEntries, getRecordKeys, getRecordValues } from
import { Material } from "./Material"; import { Material } from "./Material";
import { getKeyList } from "../utils/helpers/getKeyList"; import { getKeyList } from "../utils/helpers/getKeyList";
import { calculateMarkupMultiplier } from "./helpers"; import { calculateMarkupMultiplier } from "./helpers";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { throwIfReachable } from "../utils/helpers/throwIfReachable"; import { throwIfReachable } from "../utils/helpers/throwIfReachable";
interface DivisionParams { interface DivisionParams {
@ -538,25 +539,30 @@ export class Division {
// The amount gets re-multiplied later, so this is the correct // The amount gets re-multiplied later, so this is the correct
// amount to calculate with for "MAX". // amount to calculate with for "MAX".
const adjustedQty = mat.stored / (corpConstants.secondsPerMarketCycle * marketCycles); const adjustedQty = mat.stored / (corpConstants.secondsPerMarketCycle * marketCycles);
if (typeof mat.desiredSellAmount === "string") { /**
//Dynamically evaluated * desiredSellAmount is usually a string, but it also can be a number in old versions. eval requires a
let tmp = mat.desiredSellAmount.replace(/MAX/g, adjustedQty.toString()); * string, so we convert it to a string here, replace placeholders, and then pass it to eval.
tmp = tmp.replace(/PROD/g, mat.productionAmount.toString()); */
let temp = String(mat.desiredSellAmount);
temp = temp.replace(/MAX/g, adjustedQty.toString());
temp = temp.replace(/PROD/g, mat.productionAmount.toString());
temp = temp.replace(/INV/g, mat.stored.toString());
try { try {
sellAmt = eval?.(tmp); // Typecasting here is fine. We will validate the result immediately after this line.
} catch (e) { sellAmt = eval?.(temp) as number;
dialogBoxCreate( if (typeof sellAmt !== "number" || !Number.isFinite(sellAmt)) {
`Error evaluating your sell amount for material ${mat.name} in ${this.name}'s ${city} office. The sell amount is being set to zero`, throw new Error(`Evaluated value is not a valid number: ${sellAmt}`);
);
sellAmt = 0;
} }
} else { } catch (error) {
sellAmt = mat.desiredSellAmount; dialogBoxCreate(
`Error evaluating your sell amount for material ${mat.name} in ${this.name}'s ${city} office. Error: ${error}.`,
);
continue;
} }
// Determine the cost that the material will be sold at // Determine the cost that the material will be sold at
const markupLimit = mat.getMarkupLimit(); const markupLimit = mat.getMarkupLimit();
let sCost; let sCost: number;
if (mat.marketTa2) { if (mat.marketTa2) {
// Reverse engineer the 'maxSell' formula // Reverse engineer the 'maxSell' formula
// 1. Set 'maxSell' = sellAmt // 1. Set 'maxSell' = sellAmt
@ -590,11 +596,29 @@ export class Division {
sCost = optimalPrice; sCost = optimalPrice;
} else if (mat.marketTa1) { } else if (mat.marketTa1) {
sCost = mat.marketPrice + markupLimit; sCost = mat.marketPrice + markupLimit;
} else if (typeof mat.desiredSellPrice === "string") {
sCost = mat.desiredSellPrice.replace(/MP/g, mat.marketPrice.toString());
sCost = eval?.(sCost);
} else { } else {
sCost = mat.desiredSellPrice; // If the player does not set the price, desiredSellPrice will be an empty string.
if (mat.desiredSellPrice === "") {
continue;
}
/**
* desiredSellPrice is usually a string, but it also can be a number in old versions. eval requires a
* string, so we convert it to a string here, replace the placeholder MP, and then pass it to eval later.
*/
const temp = String(mat.desiredSellPrice).replace(/MP/g, mat.marketPrice.toString());
try {
// Typecasting here is fine. We will validate the result immediately after this line.
sCost = eval?.(temp) as number;
if (typeof sCost !== "number" || !Number.isFinite(sCost)) {
throw new Error(`Evaluated value is not a valid number: ${sCost}`);
}
} catch (error) {
dialogBoxCreate(
`Error evaluating your sell price for material ${mat.name} in ${this.name}'s ${city} office. ` +
`The sell amount is being set to zero. Error: ${error}`,
);
continue;
}
} }
mat.uiMarketPrice = sCost; mat.uiMarketPrice = sCost;
@ -658,19 +682,17 @@ export class Division {
amtStr = amtStr.replace(/IINV/g, `(${tempMaterial.stored})`); amtStr = amtStr.replace(/IINV/g, `(${tempMaterial.stored})`);
let amt = 0; let amt = 0;
try { try {
amt = eval?.(amtStr); // Typecasting here is fine. We will validate the result immediately after this line.
amt = eval?.(amtStr) as number;
if (typeof amt !== "number" || !Number.isFinite(amt)) {
throw new Error(`Evaluated value is not a valid number: ${amt}`);
}
} catch (e) { } catch (e) {
dialogBoxCreate( dialogBoxCreate(
`Calculating export for ${mat.name} in ${this.name}'s ${city} division failed with error: ${e}`, `Calculating export for ${mat.name} in ${this.name}'s ${city} division failed with error: ${e}`,
); );
continue; continue;
} }
if (isNaN(amt)) {
dialogBoxCreate(
`Error calculating export amount for ${mat.name} in ${this.name}'s ${city} division.`,
);
continue;
}
amt = amt * corpConstants.secondsPerMarketCycle * marketCycles; amt = amt * corpConstants.secondsPerMarketCycle * marketCycles;
if (mat.stored < amt) { if (mat.stored < amt) {
@ -869,35 +891,43 @@ export class Division {
const marketFactor = this.getMarketFactor(product); //Competition + demand const marketFactor = this.getMarketFactor(product); //Competition + demand
// Parse player sell-amount input (needed for TA.II and selling) // Parse player sell-amount input (needed for TA.II and selling)
let sellAmt: number | string; let sellAmt: number;
// The amount gets re-multiplied later, so this is the correct // The amount gets re-multiplied later, so this is the correct
// amount to calculate with for "MAX". // amount to calculate with for "MAX".
const adjustedQty = product.cityData[city].stored / (corpConstants.secondsPerMarketCycle * marketCycles); const adjustedQty = product.cityData[city].stored / (corpConstants.secondsPerMarketCycle * marketCycles);
const desiredSellAmount = product.cityData[city].desiredSellAmount; /**
if (typeof desiredSellAmount === "string") { * desiredSellAmount is usually a string, but it also can be a number in old versions. eval requires a
//Sell amount is dynamically evaluated * string, so we convert it to a string here, replace placeholders, and then pass it to eval.
let tmp: number | string = desiredSellAmount.replace(/MAX/g, adjustedQty.toString()); */
tmp = tmp.replace(/PROD/g, product.cityData[city].productionAmount.toString()); const desiredSellAmount = String(product.cityData[city].desiredSellAmount);
let temp = desiredSellAmount.replace(/MAX/g, adjustedQty.toString());
temp = temp.replace(/PROD/g, product.cityData[city].productionAmount.toString());
temp = temp.replace(/INV/g, product.cityData[city].stored.toString());
try { try {
tmp = eval?.(tmp); // Typecasting here is fine. We will validate the result immediately after this line.
if (typeof tmp !== "number") throw ""; sellAmt = eval?.(temp) as number;
} catch (e) { if (typeof sellAmt !== "number" || !Number.isFinite(sellAmt)) {
dialogBoxCreate( throw new Error(`Evaluated value is not a valid number: ${sellAmt}`);
`Error evaluating your sell price expression for ${product.name} in ${this.name}'s ${city} office. Sell price is being set to MAX`, }
); } catch (error) {
tmp = product.maxSellAmount; dialogBoxCreate(
`Error evaluating your sell amount for ${product.name} in ${this.name}'s ${city} office. Error: ${error}.`,
);
// break the case "SALE"
break;
} }
sellAmt = tmp;
} else if (desiredSellAmount && desiredSellAmount > 0) {
sellAmt = desiredSellAmount;
} else sellAmt = adjustedQty;
if (sellAmt < 0) sellAmt = 0; if (sellAmt < 0) {
sellAmt = 0;
}
if (product.markup === 0) {
exceptionAlert(new Error("product.markup is 0"));
product.markup = 1;
}
// Calculate Sale Cost (sCost), which could be dynamically evaluated // Calculate Sale Cost (sCost), which could be dynamically evaluated
const markupLimit = Math.max(product.cityData[city].effectiveRating, 0.001) / product.markup; const markupLimit = Math.max(product.cityData[city].effectiveRating, 0.001) / product.markup;
let sCost: number; let sCost: number;
const sellPrice = product.cityData[city].desiredSellPrice;
if (product.marketTa2) { if (product.marketTa2) {
// Reverse engineer the 'maxSell' formula // Reverse engineer the 'maxSell' formula
// 1. Set 'maxSell' = sellAmt // 1. Set 'maxSell' = sellAmt
@ -930,16 +960,29 @@ export class Division {
sCost = optimalPrice; sCost = optimalPrice;
} else if (product.marketTa1) { } else if (product.marketTa1) {
sCost = product.cityData[city].productionCost + markupLimit; sCost = product.cityData[city].productionCost + markupLimit;
} else if (typeof sellPrice === "string") {
let sCostString = sellPrice;
if (product.markup === 0) {
console.error(`mku is zero, reverting to 1 to avoid Infinity`);
product.markup = 1;
}
sCostString = sCostString.replace(/MP/g, product.cityData[city].productionCost.toString());
sCost = eval?.(sCostString);
} else { } else {
sCost = sellPrice; // If the player does not set the price, desiredSellPrice will be an empty string.
if (product.cityData[city].desiredSellPrice === "") {
// break the case "SALE"
break;
}
const temp = String(product.cityData[city].desiredSellPrice).replace(
/MP/g,
product.cityData[city].productionCost.toString(),
);
try {
// Typecasting here is fine. We will validate the result immediately after this line.
sCost = eval?.(temp) as number;
if (typeof sCost !== "number" || !Number.isFinite(sCost)) {
throw new Error(`Evaluated value is not a valid number: ${sCost}`);
}
} catch (error) {
dialogBoxCreate(
`Error evaluating your sell price for product ${product.name} in ${this.name}'s ${city} office. Error: ${error}.`,
);
// break the case "SALE"
break;
}
} }
product.uiMarketPrice[city] = sCost; product.uiMarketPrice[city] = sCost;

@ -31,10 +31,16 @@ export function SellMaterialModal(props: IProps): React.ReactElement {
} }
function onAmtChange(event: React.ChangeEvent<HTMLInputElement>): void { function onAmtChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.value === "") {
return;
}
setAmt(event.target.value); setAmt(event.target.value);
} }
function onPriceChange(event: React.ChangeEvent<HTMLInputElement>): void { function onPriceChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.value === "") {
return;
}
setPrice(event.target.value); setPrice(event.target.value);
} }

@ -21,8 +21,8 @@ interface IProps {
// Create a popup that let the player manage sales of a material // Create a popup that let the player manage sales of a material
export function SellProductModal(props: IProps): React.ReactElement { export function SellProductModal(props: IProps): React.ReactElement {
const [checked, setChecked] = useState(true); const [checked, setChecked] = useState(true);
const [iQty, setQty] = useState<string>(String(props.product.cityData[props.city].desiredSellAmount)); const [amt, setAmt] = useState<string>(String(props.product.cityData[props.city].desiredSellAmount));
const [px, setPx] = useState<string>(String(props.product.cityData[props.city].desiredSellPrice)); const [price, setPrice] = useState<string>(String(props.product.cityData[props.city].desiredSellPrice));
function onCheckedChange(event: React.ChangeEvent<HTMLInputElement>): void { function onCheckedChange(event: React.ChangeEvent<HTMLInputElement>): void {
setChecked(event.target.checked); setChecked(event.target.checked);
@ -30,7 +30,7 @@ export function SellProductModal(props: IProps): React.ReactElement {
function sellProduct(): void { function sellProduct(): void {
try { try {
actions.sellProduct(props.product, props.city, iQty, px, checked); actions.sellProduct(props.product, props.city, amt, price, checked);
} catch (error) { } catch (error) {
dialogBoxCreate(String(error)); dialogBoxCreate(String(error));
} }
@ -39,11 +39,17 @@ export function SellProductModal(props: IProps): React.ReactElement {
} }
function onAmtChange(event: React.ChangeEvent<HTMLInputElement>): void { function onAmtChange(event: React.ChangeEvent<HTMLInputElement>): void {
setQty(event.target.value); if (event.target.value === "") {
return;
}
setAmt(event.target.value);
} }
function onPriceChange(event: React.ChangeEvent<HTMLInputElement>): void { function onPriceChange(event: React.ChangeEvent<HTMLInputElement>): void {
setPx(event.target.value); if (event.target.value === "") {
return;
}
setPrice(event.target.value);
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
@ -75,14 +81,14 @@ export function SellProductModal(props: IProps): React.ReactElement {
</Typography> </Typography>
<br /> <br />
<TextField <TextField
value={iQty} value={amt}
autoFocus={true} autoFocus={true}
type="text" type="text"
placeholder="Sell amount" placeholder="Sell amount"
onChange={onAmtChange} onChange={onAmtChange}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
/> />
<TextField value={px} type="text" placeholder="Sell price" onChange={onPriceChange} onKeyDown={onKeyDown} /> <TextField value={price} type="text" placeholder="Sell price" onChange={onPriceChange} onKeyDown={onKeyDown} />
<Button onClick={sellProduct} style={{ marginLeft: ".5rem", marginRight: ".5rem" }}> <Button onClick={sellProduct} style={{ marginLeft: ".5rem", marginRight: ".5rem" }}>
Confirm Confirm
</Button> </Button>

@ -11,6 +11,8 @@ import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject";
import { import {
acceptInvestmentOffer, acceptInvestmentOffer,
buyBackShares, buyBackShares,
convertAmountString,
convertPriceString,
goPublic, goPublic,
issueNewShares, issueNewShares,
sellShares, sellShares,
@ -84,7 +86,10 @@ describe("Corporation", () => {
it("should be preserved by seed funding", () => { it("should be preserved by seed funding", () => {
const seedFunded = true; const seedFunded = true;
Player.startCorporation("TestCorp", seedFunded); Player.startCorporation("TestCorp", seedFunded);
expectSharesToAddUp(Player.corporation!); if (!Player.corporation) {
throw new Error("Player.startCorporation failed to create a corporation.");
}
expectSharesToAddUp(Player.corporation);
}); });
it("should be preserved by acceptInvestmentOffer", () => { it("should be preserved by acceptInvestmentOffer", () => {
acceptInvestmentOffer(corporation); acceptInvestmentOffer(corporation);
@ -137,4 +142,45 @@ describe("Corporation", () => {
}, },
); );
}); });
describe("convertPriceString", () => {
it("should pass normally", () => {
expect(convertPriceString("MP")).toStrictEqual("MP");
expect(convertPriceString("MP+1")).toStrictEqual("MP+1");
expect(convertPriceString("MP+MP")).toStrictEqual("MP+MP");
expect(convertPriceString("123")).toStrictEqual("123");
expect(convertPriceString("123+456")).toStrictEqual("123+456");
expect(convertPriceString("1e10")).toStrictEqual("1e10");
expect(convertPriceString("1E10")).toStrictEqual("1E10");
});
it("should throw errors", () => {
expect(() => convertPriceString("")).toThrow();
expect(() => convertPriceString("null")).toThrow();
expect(() => convertPriceString("undefined")).toThrow();
expect(() => convertPriceString("Infinity")).toThrow();
expect(() => convertPriceString("abc")).toThrow();
});
});
describe("convertAmountString", () => {
it("should pass normally", () => {
expect(convertAmountString("MAX")).toStrictEqual("MAX");
expect(convertAmountString("PROD")).toStrictEqual("PROD");
expect(convertAmountString("INV")).toStrictEqual("INV");
expect(convertAmountString("MAX+1")).toStrictEqual("MAX+1");
expect(convertAmountString("MAX+MAX")).toStrictEqual("MAX+MAX");
expect(convertAmountString("MAX+PROD+INV")).toStrictEqual("MAX+PROD+INV");
expect(convertAmountString("123")).toStrictEqual("123");
expect(convertAmountString("123+456")).toStrictEqual("123+456");
expect(convertAmountString("1e10")).toStrictEqual("1e10");
expect(convertAmountString("1E10")).toStrictEqual("1E10");
});
it("should throw errors", () => {
expect(() => convertAmountString("")).toThrow();
expect(() => convertAmountString("null")).toThrow();
expect(() => convertAmountString("undefined")).toThrow();
expect(() => convertAmountString("Infinity")).toThrow();
expect(() => convertAmountString("abc")).toThrow();
});
});
}); });