feat: Add template display name (backend) (#4966)

* Rename to nameValidator

* Refactor: NameValid

* Fix: comment

* Define new migration

* Include display_name

* Update typesGenerated.ts

* Update meta

* Adjust tests

* CLI tests

* Fix: audit

* Fix: omitempty

* site: display_name is optional

* unit: TestUsernameValid

* entities.ts: add display_name

* site: TemplateSettingsPage.test.tsx

* Fix: TemplateSettingsForm.tsx

* Adjust tests

* Add comment to display_name column

* Fix: rename

* Fix: make

* Loosen regexp

* Fix: err check

* Fix: template name length

* Allow for whitespaces

* Update migration number
This commit is contained in:
Marcin Tojek
2022-11-10 21:51:09 +01:00
committed by GitHub
parent f3eb662208
commit 2042b575dc
23 changed files with 219 additions and 57 deletions
+16 -1
View File
@@ -33,13 +33,14 @@ func init() {
}
return name
})
nameValidator := func(fl validator.FieldLevel) bool {
f := fl.Field().Interface()
str, ok := f.(string)
if !ok {
return false
}
valid := UsernameValid(str)
valid := NameValid(str)
return valid == nil
}
for _, tag := range []string{"username", "template_name", "workspace_name"} {
@@ -48,6 +49,20 @@ func init() {
panic(err)
}
}
templateDisplayNameValidator := func(fl validator.FieldLevel) bool {
f := fl.Field().Interface()
str, ok := f.(string)
if !ok {
return false
}
valid := TemplateDisplayNameValid(str)
return valid == nil
}
err := validate.RegisterValidation("template_display_name", templateDisplayNameValidator)
if err != nil {
panic(err)
}
}
// Convenience error functions don't take contexts since their responses are
@@ -11,10 +11,34 @@ import (
var (
UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$")
usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*")
templateDisplayName = regexp.MustCompile(`^[^\s](.*[^\s])?$`)
)
// UsernameValid returns whether the input string is a valid username.
func UsernameValid(str string) error {
// UsernameFrom returns a best-effort username from the provided string.
//
// It first attempts to validate the incoming string, which will
// be returned if it is valid. It then will attempt to extract
// the username from an email address. If no success happens during
// these steps, a random username will be returned.
func UsernameFrom(str string) string {
if valid := NameValid(str); valid == nil {
return str
}
emailAt := strings.LastIndex(str, "@")
if emailAt >= 0 {
str = str[:emailAt]
}
str = usernameReplace.ReplaceAllString(str, "")
if valid := NameValid(str); valid == nil {
return str
}
return strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-")
}
// NameValid returns whether the input string is a valid name.
// It is a generic validator for any name (user, workspace, template, etc.).
func NameValid(str string) error {
if len(str) > 32 {
return xerrors.New("must be <= 32 characters")
}
@@ -28,23 +52,17 @@ func UsernameValid(str string) error {
return nil
}
// UsernameFrom returns a best-effort username from the provided string.
//
// It first attempts to validate the incoming string, which will
// be returned if it is valid. It then will attempt to extract
// the username from an email address. If no success happens during
// these steps, a random username will be returned.
func UsernameFrom(str string) string {
if valid := UsernameValid(str); valid == nil {
return str
// TemplateDisplayNameValid returns whether the input string is a valid template display name.
func TemplateDisplayNameValid(str string) error {
if len(str) == 0 {
return nil // empty display_name is correct
}
emailAt := strings.LastIndex(str, "@")
if emailAt >= 0 {
str = str[:emailAt]
if len(str) > 64 {
return xerrors.New("must be <= 64 characters")
}
str = usernameReplace.ReplaceAllString(str, "")
if valid := UsernameValid(str); valid == nil {
return str
matched := templateDisplayName.MatchString(str)
if !matched {
return xerrors.New("must be alphanumeric with spaces")
}
return strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-")
return nil
}
@@ -8,7 +8,7 @@ import (
"github.com/coder/coder/coderd/httpapi"
)
func TestValid(t *testing.T) {
func TestUsernameValid(t *testing.T) {
t.Parallel()
// Tests whether usernames are valid or not.
testCases := []struct {
@@ -59,7 +59,62 @@ func TestValid(t *testing.T) {
testCase := testCase
t.Run(testCase.Username, func(t *testing.T) {
t.Parallel()
valid := httpapi.UsernameValid(testCase.Username)
valid := httpapi.NameValid(testCase.Username)
require.Equal(t, testCase.Valid, valid == nil)
})
}
}
func TestTemplateDisplayNameValid(t *testing.T) {
t.Parallel()
// Tests whether display names are valid.
testCases := []struct {
Name string
Valid bool
}{
{"", true},
{"1", true},
{"12", true},
{"1 2", true},
{"123 456", true},
{"1234 678901234567890", true},
{"<b> </b>", true},
{"S", true},
{"a1", true},
{"a1K2", true},
{"!!!!1 ?????", true},
{"k\r\rm", true},
{"abcdefghijklmnopqrst", true},
{"Wow Test", true},
{"abcdefghijklmnopqrstu-", true},
{"a1b2c3d4e5f6g7h8i9j0k-", true},
{"BANANAS_wow", true},
{"test--now", true},
{"123456789012345678901234567890123", true},
{"1234567890123456789012345678901234567890123456789012345678901234", true},
{"-a1b2c3d4e5f6g7h8i9j0k", true},
{" ", false},
{"\t", false},
{"\r\r", false},
{"\t1 ", false},
{" a", false},
{"\ra ", false},
{" 1", false},
{"1 ", false},
{" aa", false},
{"aa\r", false},
{" 12", false},
{"12 ", false},
{"\fa1", false},
{"a1\t", false},
{"12345678901234567890123456789012345678901234567890123456789012345", false},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
valid := httpapi.TemplateDisplayNameValid(testCase.Name)
require.Equal(t, testCase.Valid, valid == nil)
})
}
@@ -92,7 +147,7 @@ func TestFrom(t *testing.T) {
t.Parallel()
converted := httpapi.UsernameFrom(testCase.From)
t.Log(converted)
valid := httpapi.UsernameValid(converted)
valid := httpapi.NameValid(converted)
require.True(t, valid == nil)
if testCase.Match == "" {
require.NotEqual(t, testCase.From, converted)