feat: add user secrets client utilities (#25370)

Add frontend API methods, mocks, and form helpers for user secrets CRUD. The new client methods cover list, get, create, update, and delete requests, including URL encoding for secret names used in route paths.

Add user secret form utilities for create and update payload construction, required create field checks, and structured API validation error mapping back to form fields. User secret name validation now lives in codersdk with tests, and coderd returns field-level validation errors for create, update, and uniqueness conflicts so the frontend can show backend-owned validation results consistently.
This commit is contained in:
dylanhuff-at-coder
2026-05-19 09:30:31 -07:00
committed by GitHub
parent 01492e0e7b
commit 441854daa8
10 changed files with 816 additions and 132 deletions
+18
View File
@@ -132,6 +132,24 @@ var (
}
)
// UserSecretNameValid validates a user secret name. Names are used in
// API route path segments, so they must not include route separators.
func UserSecretNameValid(s string) error {
if strings.TrimSpace(s) == "" {
return xerrors.New("Name is required.")
}
if strings.TrimSpace(s) != s {
return xerrors.New("Name must not have leading or trailing whitespace.")
}
if strings.ContainsAny(s, "/?#") {
return xerrors.New("Name must not contain /, ?, or #.")
}
return nil
}
// UserSecretEnvNameValid validates an environment variable name for
// a user secret. Empty string is allowed (means no env injection).
func UserSecretEnvNameValid(s string) error {
+37
View File
@@ -9,6 +9,43 @@ import (
"github.com/coder/coder/v2/codersdk"
)
func TestUserSecretNameValid(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
wantErr bool
errMsg string
}{
{name: "Simple", input: "github-token"},
{name: "WithUnderscore", input: "github_token"},
{name: "WithDot", input: "github.token"},
{name: "Empty", input: "", wantErr: true, errMsg: "required"},
{name: "WhitespaceOnly", input: " ", wantErr: true, errMsg: "required"},
{name: "LeadingWhitespace", input: " github", wantErr: true, errMsg: "whitespace"},
{name: "TrailingWhitespace", input: "github ", wantErr: true, errMsg: "whitespace"},
{name: "Slash", input: "foo/bar", wantErr: true, errMsg: "must not contain"},
{name: "Question", input: "foo?bar", wantErr: true, errMsg: "must not contain"},
{name: "Fragment", input: "foo#bar", wantErr: true, errMsg: "must not contain"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := codersdk.UserSecretNameValid(tt.input)
if tt.wantErr {
assert.Error(t, err)
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestUserSecretEnvNameValid(t *testing.T) {
t.Parallel()