feat: support custom validation errors for number-typed parameters (#12224)

This commit is contained in:
Marcin Tojek
2024-02-20 16:32:03 +01:00
committed by GitHub
parent 6414b7aade
commit 57bf997369
7 changed files with 156 additions and 9 deletions
+46
View File
@@ -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()
+21
View File
@@ -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.")
}
+17
View File
@@ -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`
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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();
});
+39 -4
View File
@@ -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) || "",
);
};