mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:30:43 +00:00
58c7cdd46f
## Description: This PR centralizes all username validation using UsernameSchema with a set maximum, minimum, and a regex pattern, It also removes sanitization, as all places where the username would be sanitized on the server have been gatekept, so no unvalidated usernames can get onto the server past the ClientMessageSchema safeParse in GameServer's on message func. Here is how the errors look if that happens, Note that if the client is funtioning correctly and the user doesn't manually send a WS message, they should never see this. The screenshots are from a debug build where client uname validation was disabled. <img height="300" alt="error message too short" src="https://github.com/user-attachments/assets/1b7ac32c-2f03-40fb-8ce9-1f4ab66100bd" /> <img height="300" alt="error message bad regex" src="https://github.com/user-attachments/assets/c78b4114-7e4b-4d39-a135-4cab3ad52c0b" /> Profanity sanitization was not changed. Additionally, the censor tests were updated to reflect the new expectations. Jose was added to the jest config as an allowed transform pattern, as it didn't make sense to me to mock a zod schema. The UsernameSchema pattern was set to `^[a-zA-Z0-9_ \[\]üÜ]+$`, I personally think either we should allow all latin characters (regex has a pattern for this, `\p{L}` or `\p{sc=Latin}`) and then we'd use some kind of library to normalize all latin characters into regular ascii for name filtering, or we should only keep ascii letters. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: Lavodan (I just realized sanitization isn't a word, it's supposed to be sanitation, sorry.)
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
// Mocking the obscenity library to control its behavior in tests.
|
|
jest.mock("obscenity", () => {
|
|
return {
|
|
RegExpMatcher: class {
|
|
private dummy: string[] = ["foo", "bar", "leet", "code"];
|
|
constructor(_opts: any) {}
|
|
hasMatch(input: string): boolean {
|
|
const lower = input.toLowerCase();
|
|
const decoded = lower
|
|
.replace(/4/g, "a")
|
|
.replace(/3/g, "e")
|
|
.replace(/1/g, "i")
|
|
.replace(/0/g, "o")
|
|
.replace(/5/g, "s")
|
|
.replace(/7/g, "t");
|
|
return this.dummy.some((token) => decoded.includes(token));
|
|
}
|
|
},
|
|
collapseDuplicatesTransformer: () => ({}),
|
|
englishRecommendedTransformers: {},
|
|
englishDataset: { build: () => ({}) },
|
|
resolveConfusablesTransformer: () => ({}),
|
|
resolveLeetSpeakTransformer: () => ({}),
|
|
skipNonAlphabeticTransformer: () => ({}),
|
|
};
|
|
});
|
|
|
|
// Mocks the output of translation functions to return predictable values.
|
|
jest.mock("../src/client/Utils", () => ({
|
|
translateText: (key: string, vars?: any) =>
|
|
vars ? `${key}:${JSON.stringify(vars)}` : key,
|
|
}));
|
|
|
|
import {
|
|
fixProfaneUsername,
|
|
isProfaneUsername,
|
|
MAX_USERNAME_LENGTH,
|
|
validateUsername,
|
|
} from "../src/core/validations/username";
|
|
|
|
describe("username.ts functions", () => {
|
|
const shadowNames = [
|
|
"NicePeopleOnly",
|
|
"BeKindPlz",
|
|
"LearningManners",
|
|
"StayClassy",
|
|
"BeNicer",
|
|
"NeedHugs",
|
|
"MakeFriends",
|
|
];
|
|
|
|
describe("isProfaneUsername & fixProfaneUsername with leet decoding (mocked)", () => {
|
|
test.each([
|
|
{ username: "l33t", profane: true }, // decodes to "leet"
|
|
{ username: "L33T", profane: true },
|
|
{ username: "l33tc0de", profane: true }, // decodes to "leetcode", contains "leet" and "code"
|
|
{ username: "L33TC0DE", profane: true },
|
|
{ username: "foo123", profane: true }, // contains "foo"
|
|
{ username: "b4r", profane: true }, // decodes to "bar"
|
|
{ username: "safeName", profane: false },
|
|
{ username: "s4f3", profane: false }, // decodes to "safe" but "safe" not in dummy list
|
|
])('isProfaneUsername("%s") → %s', ({ username, profane }) => {
|
|
expect(isProfaneUsername(username)).toBe(profane);
|
|
});
|
|
|
|
test.each([
|
|
{ username: "safeName" },
|
|
{ username: "l33t" },
|
|
{ username: "b4rUser" },
|
|
])('fixProfaneUsername("%s") behavior', ({ username }) => {
|
|
const profane = isProfaneUsername(username);
|
|
const fixed = fixProfaneUsername(username);
|
|
if (!profane) {
|
|
expect(fixed).toBe(username);
|
|
} else {
|
|
// When profane: result should be one of shadowNames
|
|
expect(shadowNames).toContain(fixed);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("validateUsername", () => {
|
|
test("rejects non-string", () => {
|
|
// @ts-expect-error: Testing non-string input to validateUsername on purpose
|
|
const res = validateUsername(123);
|
|
expect(res.isValid).toBe(false);
|
|
expect(res.error).toBeDefined();
|
|
});
|
|
test("rejects too short", () => {
|
|
const res = validateUsername("ab");
|
|
expect(res.isValid).toBe(false);
|
|
});
|
|
test("rejects too long", () => {
|
|
const long = "a".repeat(MAX_USERNAME_LENGTH + 1);
|
|
const res = validateUsername(long);
|
|
expect(res.isValid).toBe(false);
|
|
});
|
|
test("rejects invalid chars", () => {
|
|
const res = validateUsername("Invalid!Name");
|
|
expect(res.isValid).toBe(false);
|
|
});
|
|
test("accepts valid ASCII names", () => {
|
|
const res = validateUsername("Good_Name123");
|
|
expect(res.isValid).toBe(true);
|
|
});
|
|
test("accepts allowed Unicode like ü", () => {
|
|
const res = validateUsername("Üser");
|
|
expect(res.isValid).toBe(true);
|
|
});
|
|
});
|
|
});
|