mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
+88
-70
@@ -20,6 +20,13 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
const (
|
||||
userSecretNameField = "name"
|
||||
userSecretValueField = "value"
|
||||
userSecretEnvNameField = "env_name"
|
||||
userSecretFilePathField = "file_path"
|
||||
)
|
||||
|
||||
// @Summary Create a new user secret
|
||||
// @ID create-a-new-user-secret
|
||||
// @Security CoderSessionToken
|
||||
@@ -49,37 +56,8 @@ func (api *API) postUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Name is required.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if req.Value == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Value is required.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := codersdk.UserSecretValueValid(req.Value); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid secret value.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := codersdk.UserSecretEnvNameValid(req.EnvName); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid environment variable name.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := codersdk.UserSecretFilePathValid(req.FilePath); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid file path.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
if validations := createUserSecretValidationErrors(req); len(validations) > 0 {
|
||||
writeUserSecretValidationErrors(ctx, rw, http.StatusBadRequest, validations)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,11 +72,8 @@ func (api *API) postUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
FilePath: req.FilePath,
|
||||
})
|
||||
if err != nil {
|
||||
if database.IsUniqueViolation(err) {
|
||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||
Message: "A secret with that name, environment variable, or file path already exists.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
if validations := userSecretConflictValidationErrors(err); len(validations) > 0 {
|
||||
writeUserSecretValidationErrors(ctx, rw, http.StatusConflict, validations)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -156,7 +131,7 @@ func (api *API) getUserSecrets(rw http.ResponseWriter, r *http.Request) { //noli
|
||||
func (api *API) getUserSecret(rw http.ResponseWriter, r *http.Request) { //nolint:revive // Method name matches route.
|
||||
ctx := r.Context()
|
||||
user := httpmw.UserParam(r)
|
||||
name := chi.URLParam(r, "name")
|
||||
name := chi.URLParam(r, userSecretNameField)
|
||||
|
||||
secret, err := api.Database.GetUserSecretByUserIDAndName(ctx, database.GetUserSecretByUserIDAndNameParams{
|
||||
UserID: user.ID,
|
||||
@@ -192,7 +167,7 @@ func (api *API) patchUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
name = chi.URLParam(r, "name")
|
||||
name = chi.URLParam(r, userSecretNameField)
|
||||
auditor = api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.UserSecret](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
@@ -214,32 +189,9 @@ func (api *API) patchUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if req.EnvName != nil {
|
||||
if err := codersdk.UserSecretEnvNameValid(*req.EnvName); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid environment variable name.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.FilePath != nil {
|
||||
if err := codersdk.UserSecretFilePathValid(*req.FilePath); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid file path.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.Value != nil {
|
||||
if err := codersdk.UserSecretValueValid(*req.Value); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid secret value.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if validations := updateUserSecretValidationErrors(req); len(validations) > 0 {
|
||||
writeUserSecretValidationErrors(ctx, rw, http.StatusBadRequest, validations)
|
||||
return
|
||||
}
|
||||
|
||||
params := database.UpdateUserSecretByUserIDAndNameParams{
|
||||
@@ -300,11 +252,8 @@ func (api *API) patchUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if database.IsUniqueViolation(err) {
|
||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||
Message: "Update would conflict with an existing secret.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
if validations := userSecretConflictValidationErrors(err); len(validations) > 0 {
|
||||
writeUserSecretValidationErrors(ctx, rw, http.StatusConflict, validations)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -337,7 +286,7 @@ func (api *API) deleteUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
name = chi.URLParam(r, "name")
|
||||
name = chi.URLParam(r, userSecretNameField)
|
||||
auditor = api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.UserSecret](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
@@ -374,6 +323,75 @@ func (api *API) deleteUserSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func writeUserSecretValidationErrors(ctx context.Context, rw http.ResponseWriter, status int, validations []codersdk.ValidationError) {
|
||||
httpapi.Write(ctx, rw, status, codersdk.Response{
|
||||
Message: "Validation failed.",
|
||||
Validations: validations,
|
||||
})
|
||||
}
|
||||
|
||||
func createUserSecretValidationErrors(req codersdk.CreateUserSecretRequest) []codersdk.ValidationError {
|
||||
var validations []codersdk.ValidationError
|
||||
validations = appendUserSecretValidationError(validations, userSecretNameField, codersdk.UserSecretNameValid(req.Name))
|
||||
if req.Value == "" {
|
||||
validations = append(validations, codersdk.ValidationError{
|
||||
Field: userSecretValueField,
|
||||
Detail: "Value is required.",
|
||||
})
|
||||
} else {
|
||||
validations = appendUserSecretValidationError(validations, userSecretValueField, codersdk.UserSecretValueValid(req.Value))
|
||||
}
|
||||
validations = appendUserSecretValidationError(validations, userSecretEnvNameField, codersdk.UserSecretEnvNameValid(req.EnvName))
|
||||
validations = appendUserSecretValidationError(validations, userSecretFilePathField, codersdk.UserSecretFilePathValid(req.FilePath))
|
||||
return validations
|
||||
}
|
||||
|
||||
func updateUserSecretValidationErrors(req codersdk.UpdateUserSecretRequest) []codersdk.ValidationError {
|
||||
var validations []codersdk.ValidationError
|
||||
if req.Value != nil {
|
||||
validations = appendUserSecretValidationError(validations, userSecretValueField, codersdk.UserSecretValueValid(*req.Value))
|
||||
}
|
||||
if req.EnvName != nil {
|
||||
validations = appendUserSecretValidationError(validations, userSecretEnvNameField, codersdk.UserSecretEnvNameValid(*req.EnvName))
|
||||
}
|
||||
if req.FilePath != nil {
|
||||
validations = appendUserSecretValidationError(validations, userSecretFilePathField, codersdk.UserSecretFilePathValid(*req.FilePath))
|
||||
}
|
||||
return validations
|
||||
}
|
||||
|
||||
func appendUserSecretValidationError(validations []codersdk.ValidationError, field string, err error) []codersdk.ValidationError {
|
||||
if err == nil {
|
||||
return validations
|
||||
}
|
||||
return append(validations, codersdk.ValidationError{
|
||||
Field: field,
|
||||
Detail: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func userSecretConflictValidationErrors(err error) []codersdk.ValidationError {
|
||||
switch {
|
||||
case database.IsUniqueViolation(err, database.UniqueUserSecretsUserNameIndex):
|
||||
return []codersdk.ValidationError{{
|
||||
Field: userSecretNameField,
|
||||
Detail: "name already in use",
|
||||
}}
|
||||
case database.IsUniqueViolation(err, database.UniqueUserSecretsUserEnvNameIndex):
|
||||
return []codersdk.ValidationError{{
|
||||
Field: userSecretEnvNameField,
|
||||
Detail: "environment variable already in use",
|
||||
}}
|
||||
case database.IsUniqueViolation(err, database.UniqueUserSecretsUserFilePathIndex):
|
||||
return []codersdk.ValidationError{{
|
||||
Field: userSecretFilePathField,
|
||||
Detail: "file path already in use",
|
||||
}}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) publishUserSecretEvent(ctx context.Context, event usersecretspubsub.Event) {
|
||||
if err := usersecretspubsub.Publish(api.Pubsub, event); err != nil {
|
||||
api.Logger.Warn(ctx, "failed to publish user secret event",
|
||||
|
||||
+98
-61
@@ -45,11 +45,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Value: "some-value",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
assert.Contains(t, sdkErr.Message, "Name is required")
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "name", "required")
|
||||
})
|
||||
|
||||
t.Run("MissingValue", func(t *testing.T) {
|
||||
@@ -59,11 +55,29 @@ func TestPostUserSecret(t *testing.T) {
|
||||
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "missing-value-secret",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
assert.Contains(t, sdkErr.Message, "Value is required")
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "value", "required")
|
||||
})
|
||||
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "foo/bar",
|
||||
Value: "some-value",
|
||||
})
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "name", "must not contain")
|
||||
})
|
||||
|
||||
t.Run("WhitespaceName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: " github",
|
||||
Value: "some-value",
|
||||
})
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "name", "whitespace")
|
||||
})
|
||||
|
||||
t.Run("DuplicateName", func(t *testing.T) {
|
||||
@@ -80,10 +94,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Name: "dup-secret",
|
||||
Value: "value2",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusConflict, sdkErr.StatusCode())
|
||||
requireSecretValidationEqualsError(t, err, http.StatusConflict, "name", "name already in use")
|
||||
})
|
||||
|
||||
t.Run("DuplicateEnvName", func(t *testing.T) {
|
||||
@@ -102,10 +113,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Value: "value2",
|
||||
EnvName: "DUPLICATE_ENV",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusConflict, sdkErr.StatusCode())
|
||||
requireSecretValidationEqualsError(t, err, http.StatusConflict, "env_name", "environment variable already in use")
|
||||
})
|
||||
|
||||
t.Run("DuplicateFilePath", func(t *testing.T) {
|
||||
@@ -124,10 +132,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Value: "value2",
|
||||
FilePath: "/tmp/dup-file",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusConflict, sdkErr.StatusCode())
|
||||
requireSecretValidationEqualsError(t, err, http.StatusConflict, "file_path", "file path already in use")
|
||||
})
|
||||
|
||||
t.Run("InvalidEnvName", func(t *testing.T) {
|
||||
@@ -139,10 +144,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Value: "value",
|
||||
EnvName: "1INVALID",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "env_name", "must start")
|
||||
})
|
||||
|
||||
t.Run("ReservedEnvName", func(t *testing.T) {
|
||||
@@ -154,10 +156,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Value: "value",
|
||||
EnvName: "PATH",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "env_name", "reserved")
|
||||
})
|
||||
|
||||
t.Run("CoderPrefixEnvName", func(t *testing.T) {
|
||||
@@ -169,10 +168,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Value: "value",
|
||||
EnvName: "CODER_AGENT_TOKEN",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "env_name", "CODER_")
|
||||
})
|
||||
|
||||
t.Run("InvalidFilePath", func(t *testing.T) {
|
||||
@@ -184,10 +180,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Value: "value",
|
||||
FilePath: "relative/path",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "file_path", "must start")
|
||||
})
|
||||
|
||||
t.Run("NullByteInValue", func(t *testing.T) {
|
||||
@@ -198,11 +191,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Name: "null-byte-secret",
|
||||
Value: "before\x00after",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
assert.Contains(t, sdkErr.Message, "Invalid secret value")
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "value", "null bytes")
|
||||
})
|
||||
|
||||
t.Run("OversizedValue", func(t *testing.T) {
|
||||
@@ -213,11 +202,7 @@ func TestPostUserSecret(t *testing.T) {
|
||||
Name: "oversized-secret",
|
||||
Value: strings.Repeat("a", codersdk.MaxSecretValueSize+1),
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
assert.Contains(t, sdkErr.Message, "Invalid secret value")
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "value", "must not exceed")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -371,10 +356,7 @@ func TestPatchUserSecret(t *testing.T) {
|
||||
_, err = client.UpdateUserSecret(ctx, codersdk.Me, "conflict-env-2", codersdk.UpdateUserSecretRequest{
|
||||
EnvName: &taken,
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusConflict, sdkErr.StatusCode())
|
||||
requireSecretValidationEqualsError(t, err, http.StatusConflict, "env_name", "environment variable already in use")
|
||||
})
|
||||
|
||||
t.Run("ConflictFilePath", func(t *testing.T) {
|
||||
@@ -398,10 +380,41 @@ func TestPatchUserSecret(t *testing.T) {
|
||||
_, err = client.UpdateUserSecret(ctx, codersdk.Me, "conflict-fp-2", codersdk.UpdateUserSecretRequest{
|
||||
FilePath: &taken,
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusConflict, sdkErr.StatusCode())
|
||||
requireSecretValidationEqualsError(t, err, http.StatusConflict, "file_path", "file path already in use")
|
||||
})
|
||||
|
||||
t.Run("InvalidEnvName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "patch-invalid-env",
|
||||
Value: "good-value",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
badEnvName := "1INVALID"
|
||||
_, err = client.UpdateUserSecret(ctx, codersdk.Me, "patch-invalid-env", codersdk.UpdateUserSecretRequest{
|
||||
EnvName: &badEnvName,
|
||||
})
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "env_name", "must start")
|
||||
})
|
||||
|
||||
t.Run("InvalidFilePath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "patch-invalid-file-path",
|
||||
Value: "good-value",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
badFilePath := "relative/path"
|
||||
_, err = client.UpdateUserSecret(ctx, codersdk.Me, "patch-invalid-file-path", codersdk.UpdateUserSecretRequest{
|
||||
FilePath: &badFilePath,
|
||||
})
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "file_path", "must start")
|
||||
})
|
||||
|
||||
t.Run("InvalidValue", func(t *testing.T) {
|
||||
@@ -418,14 +431,38 @@ func TestPatchUserSecret(t *testing.T) {
|
||||
_, err = client.UpdateUserSecret(ctx, codersdk.Me, "patch-invalid-val", codersdk.UpdateUserSecretRequest{
|
||||
Value: &badVal,
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
assert.Contains(t, sdkErr.Message, "Invalid secret value")
|
||||
requireSecretValidationContainsError(t, err, http.StatusBadRequest, "value", "null bytes")
|
||||
})
|
||||
}
|
||||
|
||||
func requireSecretValidationContainsError(t *testing.T, err error, status int, field string, detailContains string) {
|
||||
t.Helper()
|
||||
validation := requireSecretValidation(t, err, status, field)
|
||||
assert.Contains(t, validation.Detail, detailContains)
|
||||
}
|
||||
|
||||
func requireSecretValidationEqualsError(t *testing.T, err error, status int, field string, detail string) {
|
||||
t.Helper()
|
||||
validation := requireSecretValidation(t, err, status, field)
|
||||
assert.Equal(t, detail, validation.Detail)
|
||||
}
|
||||
|
||||
func requireSecretValidation(t *testing.T, err error, status int, field string) codersdk.ValidationError {
|
||||
t.Helper()
|
||||
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
assert.Equal(t, status, sdkErr.StatusCode())
|
||||
for _, validation := range sdkErr.Validations {
|
||||
if validation.Field == field {
|
||||
return validation
|
||||
}
|
||||
}
|
||||
require.Failf(t, "missing validation", "field %q not found in %#v", field, sdkErr.Validations)
|
||||
return codersdk.ValidationError{}
|
||||
}
|
||||
|
||||
func TestDeleteUserSecret(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
|
||||
Reference in New Issue
Block a user