Skip to content

Commit 4db1b1f

Browse files
committed
unify regex and update rules
1 parent 9374570 commit 4db1b1f

File tree

2 files changed

+95
-54
lines changed

2 files changed

+95
-54
lines changed

src/tools/args.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
import { z, type ZodString } from "zod";
22

3-
// Shared validation constants
4-
const NO_SLASH_REGEX = /^[^/]*$/;
5-
const NO_SLASH_ERROR = "String cannot contain '/'";
3+
const NO_UNICODE_REGEX = /^[\x20-\x7E]*$/;
4+
const NO_UNICODE_ERROR = "String cannot contain special characters or Unicode symbols";
65

6+
const ALLOWED_NAME_CHARACTERS_REGEX = /^[a-zA-Z0-9._-]+$/;
7+
export const ALLOWED_CHARACTERS_ERROR = " can only contain letters, numbers, dots, hyphens, and underscores";
8+
9+
const ALLLOWED_REGION_CHARACTERS_REGEX = /^[a-zA-Z0-9_-]+$/;
10+
export const ALLOWED_REGION_CHARACTERS_ERROR = "Region can only contain letters, numbers, hyphens, and underscores";
11+
12+
const ALLOWED_CLUSTER_NAME_CHARACTERS_REGEX = /^[a-zA-Z0-9_-]+$/;
13+
export const ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR =
14+
"Cluster names can only contain ASCII letters, numbers, and hyphens.";
15+
16+
const ALLOWED_PROJECT_NAME_CHARACTERS_REGEX = /^[a-zA-Z0-9\s()@&+:._',-]+$/;
17+
export const ALLOWED_PROJECT_NAME_CHARACTERS_ERROR =
18+
"Project names can't be longer than 64 characters and can only contain letters, numbers, spaces, and the following symbols: ( ) @ & + : . _ - ' ,";
719
export const CommonArgs = {
8-
string: (): ZodString =>
9-
z.string().regex(/^[\x20-\x7E]*$/, "String cannot contain special characters or Unicode symbols"),
20+
string: (): ZodString => z.string().regex(NO_UNICODE_REGEX, NO_UNICODE_ERROR),
1021
};
1122

1223
export const AtlasArgs = {
1324
objectId: (fieldName: string): z.ZodString =>
14-
CommonArgs.string()
25+
z
26+
.string()
1527
.min(1, `${fieldName} is required`)
1628
.regex(/^[0-9a-fA-F]{24}$/, `${fieldName} must be a valid 24-character hexadecimal string`),
1729

@@ -20,40 +32,37 @@ export const AtlasArgs = {
2032
organizationId: (): z.ZodString => AtlasArgs.objectId("organizationId"),
2133

2234
clusterName: (): z.ZodString =>
23-
CommonArgs.string()
35+
z
36+
.string()
2437
.min(1, "Cluster name is required")
2538
.max(64, "Cluster name must be 64 characters or less")
26-
.regex(NO_SLASH_REGEX, NO_SLASH_ERROR)
27-
.regex(/^[a-zA-Z0-9_-]+$/, "Cluster name can only contain letters, numbers, hyphens, and underscores"),
39+
.regex(ALLOWED_CLUSTER_NAME_CHARACTERS_REGEX, "Cluster name " + ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR),
2840

2941
projectName: (): z.ZodString =>
30-
CommonArgs.string()
42+
z
43+
.string()
3144
.min(1, "Project name is required")
3245
.max(64, "Project name must be 64 characters or less")
33-
.regex(NO_SLASH_REGEX, NO_SLASH_ERROR)
34-
.regex(/^[a-zA-Z0-9_-]+$/, "Project name can only contain letters, numbers, hyphens, and underscores"),
46+
.regex(ALLOWED_PROJECT_NAME_CHARACTERS_REGEX, ALLOWED_PROJECT_NAME_CHARACTERS_ERROR),
3547

3648
username: (): z.ZodString =>
37-
CommonArgs.string()
49+
z
50+
.string()
3851
.min(1, "Username is required")
3952
.max(100, "Username must be 100 characters or less")
40-
.regex(NO_SLASH_REGEX, NO_SLASH_ERROR)
41-
.regex(/^[a-zA-Z0-9._-]+$/, "Username can only contain letters, numbers, dots, hyphens, and underscores"),
53+
.regex(ALLOWED_NAME_CHARACTERS_REGEX, "Username " + ALLOWED_CHARACTERS_ERROR),
4254

43-
ipAddress: (): z.ZodString => CommonArgs.string().ip({ version: "v4" }),
55+
ipAddress: (): z.ZodString => z.string().ip({ version: "v4" }),
4456

45-
cidrBlock: (): z.ZodString => CommonArgs.string().cidr(),
57+
cidrBlock: (): z.ZodString => z.string().cidr(),
4658

4759
region: (): z.ZodString =>
48-
CommonArgs.string()
60+
z
61+
.string()
4962
.min(1, "Region is required")
5063
.max(50, "Region must be 50 characters or less")
51-
.regex(/^[a-zA-Z0-9_-]+$/, "Region can only contain letters, numbers, hyphens, and underscores"),
64+
.regex(ALLLOWED_REGION_CHARACTERS_REGEX, ALLOWED_REGION_CHARACTERS_ERROR),
5265

5366
password: (): z.ZodString =>
54-
CommonArgs.string()
55-
.min(1, "Password is required")
56-
.max(100, "Password must be 100 characters or less")
57-
.regex(NO_SLASH_REGEX, NO_SLASH_ERROR)
58-
.regex(/^[a-zA-Z0-9._-]+$/, "Password can only contain letters, numbers, dots, hyphens, and underscores"),
67+
z.string().min(1, "Password is required").max(100, "Password must be 100 characters or less"),
5968
};

tests/unit/args.test.ts

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { describe, expect, it } from "vitest";
2-
import { AtlasArgs, CommonArgs } from "../../src/tools/args.js";
2+
import {
3+
AtlasArgs,
4+
CommonArgs,
5+
ALLOWED_PROJECT_NAME_CHARACTERS_ERROR,
6+
ALLOWED_CHARACTERS_ERROR,
7+
ALLOWED_REGION_CHARACTERS_ERROR,
8+
ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR,
9+
} from "../../src/tools/args.js";
310

411
describe("Tool args", () => {
512
describe("CommonArgs", () => {
@@ -104,6 +111,13 @@ describe("Tool args", () => {
104111
expect(() => schema.parse("invalid")).toThrow(
105112
"projectId must be a valid 24-character hexadecimal string"
106113
);
114+
expect(() => schema.parse("507f1f77bc*86cd79943901")).toThrow(
115+
"projectId must be a valid 24-character hexadecimal string"
116+
);
117+
expect(() => schema.parse("")).toThrow("projectId is required");
118+
expect(() => schema.parse("507f1f77/bcf86cd799439011")).toThrow(
119+
"projectId must be a valid 24-character hexadecimal string"
120+
);
107121
});
108122
});
109123

@@ -144,13 +158,17 @@ describe("Tool args", () => {
144158

145159
// Invalid characters
146160
expect(() => schema.parse("cluster@name")).toThrow(
147-
"Cluster name can only contain letters, numbers, hyphens, and underscores"
161+
"Cluster name " + ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR
148162
);
149163
expect(() => schema.parse("cluster name")).toThrow(
150-
"Cluster name can only contain letters, numbers, hyphens, and underscores"
164+
"Cluster name " + ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR
151165
);
152166
expect(() => schema.parse("cluster.name")).toThrow(
153-
"Cluster name can only contain letters, numbers, hyphens, and underscores"
167+
"Cluster name " + ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR
168+
);
169+
170+
expect(() => schema.parse("cluster/name")).toThrow(
171+
"Cluster name " + ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR
154172
);
155173
});
156174

@@ -182,12 +200,8 @@ describe("Tool args", () => {
182200
expect(() => schema.parse(longUsername)).toThrow("Username must be 100 characters or less");
183201

184202
// Invalid characters
185-
expect(() => schema.parse("user@name")).toThrow(
186-
"Username can only contain letters, numbers, dots, hyphens, and underscores"
187-
);
188-
expect(() => schema.parse("user name")).toThrow(
189-
"Username can only contain letters, numbers, dots, hyphens, and underscores"
190-
);
203+
expect(() => schema.parse("user@name")).toThrow("Username " + ALLOWED_CHARACTERS_ERROR);
204+
expect(() => schema.parse("user name")).toThrow("Username " + ALLOWED_CHARACTERS_ERROR);
191205
});
192206

193207
it("should accept exactly 100 characters", () => {
@@ -278,22 +292,32 @@ describe("Tool args", () => {
278292
expect(() => schema.parse(longRegion)).toThrow("Region must be 50 characters or less");
279293

280294
// Invalid characters
281-
expect(() => schema.parse("US EAST 1")).toThrow(
282-
"Region can only contain letters, numbers, hyphens, and underscores"
283-
);
284-
expect(() => schema.parse("US.EAST.1")).toThrow(
285-
"Region can only contain letters, numbers, hyphens, and underscores"
286-
);
287-
expect(() => schema.parse("US@EAST#1")).toThrow(
288-
"Region can only contain letters, numbers, hyphens, and underscores"
289-
);
295+
expect(() => schema.parse("US EAST 1")).toThrow(ALLOWED_REGION_CHARACTERS_ERROR);
296+
expect(() => schema.parse("US.EAST.1")).toThrow(ALLOWED_REGION_CHARACTERS_ERROR);
297+
expect(() => schema.parse("US@EAST#1")).toThrow(ALLOWED_REGION_CHARACTERS_ERROR);
290298
});
291299
});
292300

293301
describe("projectName", () => {
294302
it("should validate valid project names", () => {
295303
const schema = AtlasArgs.projectName();
296-
const validNames = ["my-project", "project_1", "Project123", "test-project-2", "my_project_name"];
304+
const validNames = [
305+
"my-project",
306+
"project_1",
307+
"Project123",
308+
"test-project-2",
309+
"my_project_name",
310+
"project with spaces",
311+
"project(with)parentheses",
312+
"project@with@at",
313+
"project&with&ampersand",
314+
"project+with+plus",
315+
"project:with:colon",
316+
"project.with.dots",
317+
"project'with'apostrophe",
318+
"project,with,comma",
319+
"complex project (with) @all &symbols+here:test.name'value,",
320+
];
297321

298322
validNames.forEach((name) => {
299323
expect(schema.parse(name)).toBe(name);
@@ -302,11 +326,24 @@ describe("Tool args", () => {
302326

303327
it("should reject invalid project names", () => {
304328
const schema = AtlasArgs.projectName();
329+
330+
// Empty string
305331
expect(() => schema.parse("")).toThrow("Project name is required");
332+
333+
// Too long (over 64 characters)
306334
expect(() => schema.parse("a".repeat(65))).toThrow("Project name must be 64 characters or less");
307-
expect(() => schema.parse("invalid@name")).toThrow(
308-
"Project name can only contain letters, numbers, hyphens, and underscores"
309-
);
335+
336+
// Invalid characters not in the allowed set
337+
expect(() => schema.parse("project#with#hash")).toThrow(ALLOWED_PROJECT_NAME_CHARACTERS_ERROR);
338+
expect(() => schema.parse("project$with$dollar")).toThrow(ALLOWED_PROJECT_NAME_CHARACTERS_ERROR);
339+
expect(() => schema.parse("project!with!exclamation")).toThrow(ALLOWED_PROJECT_NAME_CHARACTERS_ERROR);
340+
expect(() => schema.parse("project[with]brackets")).toThrow(ALLOWED_PROJECT_NAME_CHARACTERS_ERROR);
341+
});
342+
343+
it("should accept exactly 64 characters", () => {
344+
const schema = AtlasArgs.projectName();
345+
const maxLengthName = "a".repeat(64);
346+
expect(schema.parse(maxLengthName)).toBe(maxLengthName);
310347
});
311348
});
312349

@@ -324,9 +361,6 @@ describe("Tool args", () => {
324361
const schema = AtlasArgs.password();
325362
expect(() => schema.parse("")).toThrow("Password is required");
326363
expect(() => schema.parse("a".repeat(101))).toThrow("Password must be 100 characters or less");
327-
expect(() => schema.parse("invalid password")).toThrow(
328-
"Password can only contain letters, numbers, dots, hyphens, and underscores"
329-
);
330364
});
331365
});
332366
});
@@ -366,16 +400,14 @@ describe("Tool args", () => {
366400
"Cluster name must be 64 characters or less"
367401
);
368402
expect(() => AtlasArgs.clusterName().parse("invalid@name")).toThrow(
369-
"Cluster name can only contain letters, numbers, hyphens, and underscores"
403+
"Cluster name " + ALLOWED_CLUSTER_NAME_CHARACTERS_ERROR
370404
);
371405

372406
expect(() => AtlasArgs.username().parse("")).toThrow("Username is required");
373407
expect(() => AtlasArgs.username().parse("a".repeat(101))).toThrow(
374408
"Username must be 100 characters or less"
375409
);
376-
expect(() => AtlasArgs.username().parse("invalid name")).toThrow(
377-
"Username can only contain letters, numbers, dots, hyphens, and underscores"
378-
);
410+
expect(() => AtlasArgs.username().parse("invalid name")).toThrow("Username " + ALLOWED_CHARACTERS_ERROR);
379411
});
380412
});
381413
});

0 commit comments

Comments
 (0)