Validate and deduplicate custom difficulty presets
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 6m4s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 6m4s
This commit is contained in:
86
tests/difficulty-presets.test.ts
Normal file
86
tests/difficulty-presets.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { customDifficultySchema } from "../shared/protocol";
|
||||
import { mergeDifficultyPresets } from "../shared/difficulty-presets";
|
||||
|
||||
const rules = { seconds: 90, zoom: true };
|
||||
test("difficulty names accept normal text and count Unicode characters consistently with PostgreSQL", () => {
|
||||
for (const name of [
|
||||
"Normal",
|
||||
"Speed 90",
|
||||
"Тест",
|
||||
"中文",
|
||||
"😎😎",
|
||||
"😎".repeat(40),
|
||||
"a".repeat(40),
|
||||
"a\\b",
|
||||
])
|
||||
assert.equal(
|
||||
customDifficultySchema.safeParse({ name, ...rules }).success,
|
||||
true,
|
||||
name,
|
||||
);
|
||||
assert.equal(
|
||||
customDifficultySchema.parse({ name: " My challenge ", ...rules }).name,
|
||||
"My challenge",
|
||||
);
|
||||
for (const name of [
|
||||
"",
|
||||
"a",
|
||||
"😎",
|
||||
"a".repeat(41),
|
||||
"😎".repeat(41),
|
||||
"x\nname",
|
||||
"x\u0085name",
|
||||
"x\u202ename",
|
||||
"x\u2066name",
|
||||
])
|
||||
assert.equal(
|
||||
customDifficultySchema.safeParse({ name, ...rules }).success,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("difficulty rules enforce their supported range and strict request shape", () => {
|
||||
for (const seconds of [15, 90, 600])
|
||||
assert.equal(
|
||||
customDifficultySchema.safeParse({ name: "Fixture", ...rules, seconds })
|
||||
.success,
|
||||
true,
|
||||
);
|
||||
for (const seconds of [0, 14, 16, 601, 15.5, NaN, Infinity, "90"])
|
||||
assert.equal(
|
||||
customDifficultySchema.safeParse({ name: "Fixture", ...rules, seconds })
|
||||
.success,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
customDifficultySchema.safeParse({
|
||||
name: "Fixture",
|
||||
...rules,
|
||||
owner: "forged",
|
||||
}).success,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("a delayed preset listing cannot erase a newly saved preset or duplicate it", async () => {
|
||||
const old = { id: "old", name: "Old", ...rules };
|
||||
const saved = { id: "saved", name: "New", ...rules };
|
||||
let release!: (presets: (typeof old)[]) => void;
|
||||
const response = new Promise<(typeof old)[]>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let presets = [old];
|
||||
const loading = response.then((snapshot) => {
|
||||
presets = mergeDifficultyPresets(presets, snapshot);
|
||||
});
|
||||
presets = mergeDifficultyPresets(presets, [saved]);
|
||||
release([old]);
|
||||
await loading;
|
||||
assert.deepEqual(
|
||||
presets.map((p) => p.id),
|
||||
["saved", "old"],
|
||||
);
|
||||
assert.equal(mergeDifficultyPresets(presets, [saved]).length, 2);
|
||||
});
|
||||
@@ -1,18 +1,58 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { checkServiceError, ServiceError } from "../lib/service-error";
|
||||
import {
|
||||
checkServiceError,
|
||||
checkDifficultyPresetError,
|
||||
ServiceError,
|
||||
} from "../lib/service-error";
|
||||
|
||||
test("expected preset validation and duplicate-name errors return 400/409 without database log noise", (t) => {
|
||||
const log = t.mock.method(console, "error", () => {});
|
||||
for (const [code, status] of [
|
||||
["23514", 400],
|
||||
["23505", 409],
|
||||
] as const)
|
||||
assert.throws(
|
||||
() =>
|
||||
checkDifficultyPresetError({
|
||||
code,
|
||||
message: "private database constraint details",
|
||||
}),
|
||||
(error) => {
|
||||
assert.ok(error instanceof ServiceError);
|
||||
assert.equal(error.status, status);
|
||||
assert.doesNotMatch(error.message, /private database|constraint/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
assert.equal(log.mock.callCount(), 0);
|
||||
assert.doesNotThrow(() => checkDifficultyPresetError(null));
|
||||
});
|
||||
|
||||
test("backend failures use 503 and keep database internals out of browser errors", (t) => {
|
||||
t.mock.method(console, "error", () => {});
|
||||
assert.throws(() => checkServiceError({ code: "42P01", message: 'relation "private_table" does not exist' }), (error) => {
|
||||
assert.ok(error instanceof ServiceError);
|
||||
assert.equal(error.status, 503);
|
||||
assert.doesNotMatch(error.message, /private_table|42P01/);
|
||||
return true;
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
checkServiceError({
|
||||
code: "42P01",
|
||||
message: 'relation "private_table" does not exist',
|
||||
}),
|
||||
(error) => {
|
||||
assert.ok(error instanceof ServiceError);
|
||||
assert.equal(error.status, 503);
|
||||
assert.doesNotMatch(error.message, /private_table|42P01/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
test("missing records and expected RPC validation keep actionable statuses and messages", () => {
|
||||
assert.throws(() => checkServiceError({ code: "PGRST116", message: "no rows" }), (error) => error instanceof ServiceError && error.status === 404);
|
||||
assert.throws(() => checkServiceError({ code: "P0001", message: "Guess outside map" }), /Guess outside map/);
|
||||
assert.throws(
|
||||
() => checkServiceError({ code: "PGRST116", message: "no rows" }),
|
||||
(error) => error instanceof ServiceError && error.status === 404,
|
||||
);
|
||||
assert.throws(
|
||||
() => checkServiceError({ code: "P0001", message: "Guess outside map" }),
|
||||
/Guess outside map/,
|
||||
);
|
||||
assert.doesNotThrow(() => checkServiceError(null));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user