mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: support custom validation errors for number-typed parameters (#12224)
This commit is contained in:
@@ -556,6 +556,14 @@ func TestCreateValidateRichParameters(t *testing.T) {
|
||||
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: ptr.Ref(int32(3)), ValidationMax: ptr.Ref(int32(10))},
|
||||
}
|
||||
|
||||
numberCustomErrorRichParameters := []*proto.RichParameter{
|
||||
{
|
||||
Name: numberParameterName, Type: "number", Mutable: true,
|
||||
ValidationMin: ptr.Ref(int32(3)), ValidationMax: ptr.Ref(int32(10)),
|
||||
ValidationError: "These are values: {min}, {max}, and {value}.",
|
||||
},
|
||||
}
|
||||
|
||||
stringRichParameters := []*proto.RichParameter{
|
||||
{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
|
||||
}
|
||||
@@ -644,6 +652,44 @@ func TestCreateValidateRichParameters(t *testing.T) {
|
||||
<-doneChan
|
||||
})
|
||||
|
||||
t.Run("ValidateNumber_CustomError", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(numberCustomErrorRichParameters))
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||||
|
||||
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
|
||||
clitest.SetupConfig(t, member, root)
|
||||
doneChan := make(chan struct{})
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := inv.Run()
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
numberParameterName, "12",
|
||||
"These are values: 3, 10, and 12.", "",
|
||||
"Enter a value", "8",
|
||||
"Confirm create?", "yes",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
value := matches[i+1]
|
||||
pty.ExpectMatch(match)
|
||||
if value != "" {
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
}
|
||||
<-doneChan
|
||||
})
|
||||
|
||||
t.Run("ValidateBool", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -376,3 +376,24 @@ func TestParameterResolver_ValidateResolve_Ephemeral_UseEmptyDefault(t *testing.
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", v)
|
||||
}
|
||||
|
||||
func TestParameterResolver_ValidateResolve_Number_CustomError(t *testing.T) {
|
||||
t.Parallel()
|
||||
uut := codersdk.ParameterResolver{}
|
||||
p := codersdk.TemplateVersionParameter{
|
||||
Name: "n",
|
||||
Type: "number",
|
||||
Mutable: true,
|
||||
DefaultValue: "5",
|
||||
|
||||
ValidationMin: ptr.Ref(int32(4)),
|
||||
ValidationMax: ptr.Ref(int32(6)),
|
||||
ValidationError: "These are values for testing purposes: {min}, {max}, and {value}.",
|
||||
}
|
||||
_, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
|
||||
Name: "n",
|
||||
Value: "8",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "These are values for testing purposes: 4, 6, and 8.")
|
||||
}
|
||||
|
||||
Vendored
+17
@@ -249,6 +249,23 @@ data "coder_parameter" "instances" {
|
||||
}
|
||||
```
|
||||
|
||||
It is possible to override the default `error` message for a `number` parameter,
|
||||
along with its associated `min` and/or `max` properties. The following message
|
||||
placeholders are available `{min}`, `{max}`, and `{value}`.
|
||||
|
||||
```hcl
|
||||
data "coder_parameter" "instances" {
|
||||
name = "Instances"
|
||||
type = "number"
|
||||
description = "Number of compute instances"
|
||||
validation {
|
||||
min = 1
|
||||
max = 4
|
||||
error = "Sorry, we can't provision too many instances - maximum limit: {max}, wanted: {value}."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### String
|
||||
|
||||
You can validate a `string` parameter to match a regular expression. The `regex`
|
||||
|
||||
@@ -96,7 +96,7 @@ require (
|
||||
github.com/coder/flog v1.1.0
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0
|
||||
github.com/coder/retry v1.5.1
|
||||
github.com/coder/terraform-provider-coder v0.17.0
|
||||
github.com/coder/terraform-provider-coder v0.18.0
|
||||
github.com/coder/wgtunnel v0.1.13-0.20231127054351-578bfff9b92a
|
||||
github.com/coreos/go-oidc/v3 v3.9.0
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
|
||||
@@ -204,8 +204,8 @@ github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuO
|
||||
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ=
|
||||
github.com/coder/tailscale v1.1.1-0.20240214140224-3788ab894ba1 h1:A7dZHNidAVH6Kxn5D3hTEH+iRO8slnM0aRer6/cxlyE=
|
||||
github.com/coder/tailscale v1.1.1-0.20240214140224-3788ab894ba1/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4=
|
||||
github.com/coder/terraform-provider-coder v0.17.0 h1:qwdLSbh6vPN+QDDvw1WNSYYEFlFwJFwzzP9vrvwr/ks=
|
||||
github.com/coder/terraform-provider-coder v0.17.0/go.mod h1:pACHRoXSHBGyY696mLeQ1hR/Ag1G2wFk5bw0mT5Zp2g=
|
||||
github.com/coder/terraform-provider-coder v0.18.0 h1:JWSBsOuzyiCev3C2Aj8Y1dvJkm5JMysIrIylMJtzPAY=
|
||||
github.com/coder/terraform-provider-coder v0.18.0/go.mod h1:pACHRoXSHBGyY696mLeQ1hR/Ag1G2wFk5bw0mT5Zp2g=
|
||||
github.com/coder/wgtunnel v0.1.13-0.20231127054351-578bfff9b92a h1:KhR9LUVllMZ+e9lhubZ1HNrtJDgH5YLoTvpKwmrGag4=
|
||||
github.com/coder/wgtunnel v0.1.13-0.20231127054351-578bfff9b92a/go.mod h1:QzfptVUdEO+XbkzMKx1kw13i9wwpJlfI1RrZ6SNZ0hA=
|
||||
github.com/coder/wireguard-go v0.0.0-20230807234434-d825b45ccbf5 h1:eDk/42Kj4xN4yfE504LsvcFEo3dWUiCOaBiWJ2uIH2A=
|
||||
|
||||
@@ -25,7 +25,6 @@ import { rest } from "msw";
|
||||
const nameLabelText = "Workspace Name";
|
||||
const createWorkspaceText = "Create Workspace";
|
||||
const validationNumberNotInRangeText = "Value must be between 1 and 3.";
|
||||
const validationPatternNotMatched = `${MockTemplateVersionParameter3.validation_error} (value does not match the pattern ^[a-z]{3}$)`;
|
||||
|
||||
const renderCreateWorkspacePage = () => {
|
||||
return renderWithAuth(<CreateWorkspacePage />, {
|
||||
@@ -152,7 +151,36 @@ describe("CreateWorkspacePage", () => {
|
||||
fireEvent.submit(thirdParameterField);
|
||||
|
||||
const validationError = await screen.findByText(
|
||||
validationPatternNotMatched,
|
||||
MockTemplateVersionParameter3.validation_error as string,
|
||||
);
|
||||
expect(validationError).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("rich parameter: number validation fails with custom error", async () => {
|
||||
jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
{
|
||||
...MockTemplateVersionParameter2,
|
||||
validation_error: "These are values: {min}, {max}, and {value}.",
|
||||
validation_monotonic: undefined, // only needs min-max rules
|
||||
},
|
||||
]);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const secondParameterField = await screen.findByLabelText(
|
||||
MockTemplateVersionParameter2.name,
|
||||
{ exact: false },
|
||||
);
|
||||
expect(secondParameterField).toBeDefined();
|
||||
fireEvent.change(secondParameterField, {
|
||||
target: { value: "4" },
|
||||
});
|
||||
fireEvent.submit(secondParameterField);
|
||||
|
||||
const validationError = await screen.findByText(
|
||||
"These are values: 1, 3, and 4.",
|
||||
);
|
||||
expect(validationError).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -79,7 +79,9 @@ export const useValidationSchemaForRichParameters = (
|
||||
if (Number(val) < templateParameter.validation_min) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: `Value must be greater than ${templateParameter.validation_min}.`,
|
||||
message:
|
||||
parameterError(templateParameter, val) ??
|
||||
`Value must be greater than ${templateParameter.validation_min}.`,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
@@ -89,7 +91,9 @@ export const useValidationSchemaForRichParameters = (
|
||||
if (templateParameter.validation_max < Number(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: `Value must be less than ${templateParameter.validation_max}.`,
|
||||
message:
|
||||
parameterError(templateParameter, val) ??
|
||||
`Value must be less than ${templateParameter.validation_max}.`,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
@@ -102,7 +106,9 @@ export const useValidationSchemaForRichParameters = (
|
||||
) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: `Value must be between ${templateParameter.validation_min} and ${templateParameter.validation_max}.`,
|
||||
message:
|
||||
parameterError(templateParameter, val) ??
|
||||
`Value must be between ${templateParameter.validation_min} and ${templateParameter.validation_max}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -149,7 +155,7 @@ export const useValidationSchemaForRichParameters = (
|
||||
if (val && !regex.test(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: `${templateParameter.validation_error} (value does not match the pattern ${templateParameter.validation_regex})`,
|
||||
message: parameterError(templateParameter, val),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -162,3 +168,32 @@ export const useValidationSchemaForRichParameters = (
|
||||
)
|
||||
.required();
|
||||
};
|
||||
|
||||
const parameterError = (
|
||||
parameter: TemplateVersionParameter,
|
||||
value?: string,
|
||||
): string | undefined => {
|
||||
if (!parameter.validation_error || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = new Map<string, string>([
|
||||
[
|
||||
"{min}",
|
||||
parameter.validation_min !== undefined
|
||||
? parameter.validation_min.toString()
|
||||
: "",
|
||||
],
|
||||
[
|
||||
"{max}",
|
||||
parameter.validation_max !== undefined
|
||||
? parameter.validation_max.toString()
|
||||
: "",
|
||||
],
|
||||
["{value}", value],
|
||||
]);
|
||||
return parameter.validation_error.replace(
|
||||
/{min}|{max}|{value}/g,
|
||||
(match) => r.get(match) || "",
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user