Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 14cdd85

Browse files
authored
fix(site): username validation in forms (coder#1851)
* refactor(site): move name validation to utils * fix(site): username validation in forms
1 parent 8a5277e commit 14cdd85

File tree

6 files changed

+72
-59
lines changed

6 files changed

+72
-59
lines changed

site/src/components/CreateUserForm/CreateUserForm.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { FormikContextType, FormikErrors, useFormik } from "formik"
44
import React from "react"
55
import * as Yup from "yup"
66
import * as TypesGen from "../../api/typesGenerated"
7-
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
7+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
88
import { FormFooter } from "../FormFooter/FormFooter"
99
import { FullPageForm } from "../FullPageForm/FullPageForm"
1010

@@ -15,7 +15,6 @@ export const Language = {
1515
emailInvalid: "Please enter a valid email address.",
1616
emailRequired: "Please enter an email address.",
1717
passwordRequired: "Please enter a password.",
18-
usernameRequired: "Please enter a username.",
1918
createUser: "Create",
2019
cancel: "Cancel",
2120
}
@@ -32,7 +31,7 @@ export interface CreateUserFormProps {
3231
const validationSchema = Yup.object({
3332
email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired),
3433
password: Yup.string().required(Language.passwordRequired),
35-
username: Yup.string().required(Language.usernameRequired),
34+
username: nameValidator(Language.usernameLabel),
3635
})
3736

3837
export const CreateUserForm: React.FC<CreateUserFormProps> = ({

site/src/components/SettingsAccountForm/SettingsAccountForm.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import TextField from "@material-ui/core/TextField"
33
import { FormikContextType, FormikErrors, useFormik } from "formik"
44
import React from "react"
55
import * as Yup from "yup"
6-
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
6+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
77
import { LoadingButton } from "../LoadingButton/LoadingButton"
88
import { Stack } from "../Stack/Stack"
99

@@ -22,7 +22,7 @@ export const Language = {
2222

2323
const validationSchema = Yup.object({
2424
email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired),
25-
username: Yup.string().trim(),
25+
username: nameValidator(Language.usernameLabel),
2626
})
2727

2828
export type AccountFormErrors = FormikErrors<AccountFormValues>
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { screen, waitFor } from "@testing-library/react"
22
import userEvent from "@testing-library/user-event"
33
import React from "react"
4-
import { reach, StringSchema } from "yup"
54
import * as API from "../../api/api"
65
import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter"
76
import { MockTemplate, MockWorkspace } from "../../testHelpers/entities"
87
import { renderWithAuth } from "../../testHelpers/renderHelpers"
8+
import { Language as FormLanguage } from "../../util/formUtils"
99
import CreateWorkspacePage from "./CreateWorkspacePage"
10-
import { Language, validationSchema } from "./CreateWorkspacePageView"
10+
import { Language } from "./CreateWorkspacePageView"
1111

1212
const renderCreateWorkspacePage = () => {
1313
return renderWithAuth(<CreateWorkspacePage />, {
@@ -23,8 +23,6 @@ const fillForm = async ({ name = "example" }: { name?: string }) => {
2323
await userEvent.click(submitButton)
2424
}
2525

26-
const nameSchema = reach(validationSchema, "name") as StringSchema
27-
2826
describe("CreateWorkspacePage", () => {
2927
it("renders", async () => {
3028
renderCreateWorkspacePage()
@@ -35,7 +33,7 @@ describe("CreateWorkspacePage", () => {
3533
it("shows validation error message", async () => {
3634
renderCreateWorkspacePage()
3735
await fillForm({ name: "$$$" })
38-
const errorMessage = await screen.findByText(Language.nameMatches)
36+
const errorMessage = await screen.findByText(FormLanguage.nameInvalidChars(Language.nameLabel))
3937
expect(errorMessage).toBeDefined()
4038
})
4139

@@ -47,38 +45,4 @@ describe("CreateWorkspacePage", () => {
4745
// Check if the request was made
4846
await waitFor(() => expect(API.createWorkspace).toBeCalledTimes(1))
4947
})
50-
51-
describe("validationSchema", () => {
52-
it("allows a 1-letter name", () => {
53-
const validate = () => nameSchema.validateSync("t")
54-
expect(validate).not.toThrow()
55-
})
56-
57-
it("allows a 32-letter name", () => {
58-
const input = Array(32).fill("a").join("")
59-
const validate = () => nameSchema.validateSync(input)
60-
expect(validate).not.toThrow()
61-
})
62-
63-
it("allows 'test-3' to be used as name", () => {
64-
const validate = () => nameSchema.validateSync("test-3")
65-
expect(validate).not.toThrow()
66-
})
67-
68-
it("allows '3-test' to be used as a name", () => {
69-
const validate = () => nameSchema.validateSync("3-test")
70-
expect(validate).not.toThrow()
71-
})
72-
73-
it("disallows a 33-letter name", () => {
74-
const input = Array(33).fill("a").join("")
75-
const validate = () => nameSchema.validateSync(input)
76-
expect(validate).toThrow()
77-
})
78-
79-
it("disallows a space", () => {
80-
const validate = () => nameSchema.validateSync("test 3")
81-
expect(validate).toThrow()
82-
})
83-
})
8448
})

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

+2-14
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,13 @@ import { Loader } from "../../components/Loader/Loader"
1010
import { Margins } from "../../components/Margins/Margins"
1111
import { ParameterInput } from "../../components/ParameterInput/ParameterInput"
1212
import { Stack } from "../../components/Stack/Stack"
13-
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
13+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
1414

1515
export const Language = {
1616
templateLabel: "Template",
1717
nameLabel: "Name",
18-
nameRequired: "Please enter a name.",
19-
nameMatches: "Name must start with a-Z or 0-9 and can contain a-Z, 0-9 or -",
20-
nameMax: "Name cannot be longer than 32 characters",
2118
}
2219

23-
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40
24-
const maxLenName = 32
25-
26-
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18
27-
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/
28-
2920
export interface CreateWorkspacePageViewProps {
3021
loadingTemplates: boolean
3122
loadingTemplateSchema: boolean
@@ -39,10 +30,7 @@ export interface CreateWorkspacePageViewProps {
3930
}
4031

4132
export const validationSchema = Yup.object({
42-
name: Yup.string()
43-
.required(Language.nameRequired)
44-
.matches(usernameRE, Language.nameMatches)
45-
.max(maxLenName, Language.nameMax),
33+
name: nameValidator(Language.nameLabel),
4634
})
4735

4836
export const CreateWorkspacePageView: React.FC<CreateWorkspacePageViewProps> = (props) => {

site/src/util/formUtils.test.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FormikContextType } from "formik/dist/types"
2-
import { getFormHelpers, onChangeTrimmed } from "./formUtils"
2+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "./formUtils"
33

44
interface TestType {
55
untouchedGoodField: string
@@ -35,6 +35,8 @@ const form = {
3535
},
3636
} as unknown as FormikContextType<TestType>
3737

38+
const nameSchema = nameValidator("name")
39+
3840
describe("form util functions", () => {
3941
describe("getFormHelpers", () => {
4042
describe("without API errors", () => {
@@ -94,4 +96,38 @@ describe("form util functions", () => {
9496
expect(mockHandleChange).toHaveBeenCalledWith({ target: { value: "hello" } })
9597
})
9698
})
99+
100+
describe("nameValidator", () => {
101+
it("allows a 1-letter name", () => {
102+
const validate = () => nameSchema.validateSync("a")
103+
expect(validate).not.toThrow()
104+
})
105+
106+
it("allows a 32-letter name", () => {
107+
const input = Array(32).fill("a").join("")
108+
const validate = () => nameSchema.validateSync(input)
109+
expect(validate).not.toThrow()
110+
})
111+
112+
it("allows 'test-3' to be used as name", () => {
113+
const validate = () => nameSchema.validateSync("test-3")
114+
expect(validate).not.toThrow()
115+
})
116+
117+
it("allows '3-test' to be used as a name", () => {
118+
const validate = () => nameSchema.validateSync("3-test")
119+
expect(validate).not.toThrow()
120+
})
121+
122+
it("disallows a 33-letter name", () => {
123+
const input = Array(33).fill("a").join("")
124+
const validate = () => nameSchema.validateSync(input)
125+
expect(validate).toThrow()
126+
})
127+
128+
it("disallows a space", () => {
129+
const validate = () => nameSchema.validateSync("test 3")
130+
expect(validate).toThrow()
131+
})
132+
})
97133
})

site/src/util/formUtils.ts

+26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
import { FormikContextType, FormikErrors, getIn } from "formik"
22
import { ChangeEvent, ChangeEventHandler, FocusEventHandler, ReactNode } from "react"
3+
import * as Yup from "yup"
4+
5+
export const Language = {
6+
nameRequired: (name: string): string => {
7+
return `Please enter a ${name.toLowerCase()}.`
8+
},
9+
nameInvalidChars: (name: string): string => {
10+
return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -`
11+
},
12+
nameTooLong: (name: string): string => {
13+
return `${name} cannot be longer than 32 characters`
14+
},
15+
}
316

417
interface FormHelpers {
518
name: string
@@ -38,3 +51,16 @@ export const onChangeTrimmed =
3851
event.target.value = event.target.value.trim()
3952
form.handleChange(event)
4053
}
54+
55+
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40
56+
const maxLenName = 32
57+
58+
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18
59+
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/
60+
61+
// REMARK: see #1756 for name/username semantics
62+
export const nameValidator = (name: string): Yup.StringSchema =>
63+
Yup.string()
64+
.required(Language.nameRequired(name))
65+
.matches(usernameRE, Language.nameInvalidChars(name))
66+
.max(maxLenName, Language.nameTooLong(name))

0 commit comments

Comments
 (0)