feat!: Validate monotonic numbers for rich parameters (#6046)

* Database changes

* protobuf

* Fix: docs

* workspaces_test

* Validation in coderd

* Fix: resources

* omitempty

* UI changes

* UI tests

* fix
This commit is contained in:
Marcin Tojek
2023-02-07 09:36:13 +01:00
committed by GitHub
parent e3ae664a29
commit b86bce8494
32 changed files with 917 additions and 392 deletions
+12 -4
View File
@@ -758,10 +758,11 @@ export interface TemplateVersionParameter {
readonly default_value: string
readonly icon: string
readonly options: TemplateVersionParameterOption[]
readonly validation_error: string
readonly validation_regex: string
readonly validation_min: number
readonly validation_max: number
readonly validation_error?: string
readonly validation_regex?: string
readonly validation_min?: number
readonly validation_max?: number
readonly validation_monotonic?: ValidationMonotonicOrder
}
// From codersdk/templateversions.go
@@ -1209,6 +1210,13 @@ export const TemplateRoles: TemplateRole[] = ["", "admin", "use"]
export type UserStatus = "active" | "suspended"
export const UserStatuses: UserStatus[] = ["active", "suspended"]
// From codersdk/templateversions.go
export type ValidationMonotonicOrder = "decreasing" | "increasing"
export const ValidationMonotonicOrders: ValidationMonotonicOrder[] = [
"decreasing",
"increasing",
]
// From codersdk/workspaceagents.go
export type WorkspaceAgentLifecycle =
| "created"
@@ -29,6 +29,7 @@ const createTemplateVersionParameter = (
validation_regex: "",
validation_min: 0,
validation_max: 0,
validation_monotonic: "",
...partial,
}
@@ -5,5 +5,7 @@
"validationRequiredParameter": "Parameter is required.",
"validationNumberNotInRange": "Value must be between {{min}} and {{max}}.",
"validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}}).",
"updateWorkspace": "Update workspace"
"updateWorkspace": "Update workspace",
"validationNumberNotIncreasing": "The value must be equal or greater than the previous one {{last}}.",
"validationNumberNotDecreasing": "The value must be equal or lower than the previous one {{last}}."
}
@@ -493,6 +493,7 @@ export const workspaceBuildParameterValue = (
export const ValidationSchemaForRichParameters = (
ns: string,
templateParameters?: TypesGen.TemplateVersionParameter[],
lastBuildParameters?: TypesGen.WorkspaceBuildParameter[],
): Yup.AnySchema => {
const { t } = useTranslation(ns)
@@ -515,28 +516,62 @@ export const ValidationSchemaForRichParameters = (
switch (templateParameter.type) {
case "number":
if (
templateParameter.validation_min === 0 &&
templateParameter.validation_max === 0
templateParameter.validation_min &&
templateParameter.validation_max
) {
return true
if (
Number(val) < templateParameter.validation_min ||
templateParameter.validation_max < Number(val)
) {
return ctx.createError({
path: ctx.path,
message: t("validationNumberNotInRange", {
min: templateParameter.validation_min,
max: templateParameter.validation_max,
}),
})
}
}
if (
Number(val) < templateParameter.validation_min ||
templateParameter.validation_max < Number(val)
templateParameter.validation_monotonic &&
lastBuildParameters
) {
return ctx.createError({
path: ctx.path,
message: t("validationNumberNotInRange", {
min: templateParameter.validation_min,
max: templateParameter.validation_max,
}),
})
const lastBuildParameter = lastBuildParameters.find(
(last) => last.name === name,
)
if (lastBuildParameter) {
switch (templateParameter.validation_monotonic) {
case "increasing":
if (Number(lastBuildParameter.value) > Number(val)) {
return ctx.createError({
path: ctx.path,
message: t("validationNumberNotIncreasing", {
last: lastBuildParameter.value,
}),
})
}
break
case "decreasing":
if (Number(lastBuildParameter.value) < Number(val)) {
return ctx.createError({
path: ctx.path,
message: t("validationNumberNotDecreasing", {
last: lastBuildParameter.value,
}),
})
}
break
}
}
}
break
case "string":
{
if (templateParameter.validation_regex.length === 0) {
if (
!templateParameter.validation_regex ||
templateParameter.validation_regex.length === 0
) {
return true
}
@@ -2,9 +2,11 @@ import { fireEvent, screen } from "@testing-library/react"
import {
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
MockTemplateVersionParameter5,
MockWorkspace,
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
MockWorkspaceBuildParameter5,
renderWithAuth,
} from "testHelpers/renderHelpers"
import * as API from "api/api"
@@ -19,6 +21,14 @@ const validationNumberNotInRangeText = t("validationNumberNotInRange", {
min: "1",
max: "3",
})
const validationNumberNotIncreasing = t("validationNumberNotIncreasing", {
ns: "workspaceBuildParametersPage",
last: "3",
})
const validationNumberNotDecreasing = t("validationNumberNotDecreasing", {
ns: "workspaceBuildParametersPage",
last: "5",
})
const renderWorkspaceBuildParametersPage = () => {
return renderWithAuth(<WorkspaceBuildParametersPage />, {
@@ -116,4 +126,80 @@ describe("WorkspaceBuildParametersPage", () => {
)
expect(validationError).toBeDefined()
})
it("rich parameter: number is not monotonically increasing", async () => {
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValueOnce([
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
])
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValueOnce([
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
])
renderWorkspaceBuildParametersPage()
const element = await screen.findByText(pageTitleText)
expect(element).toBeDefined()
const secondParameter = await screen.findByText(
MockTemplateVersionParameter2.description,
)
expect(secondParameter).toBeDefined()
const secondParameterField = await screen.findByLabelText(
MockTemplateVersionParameter2.name,
)
expect(secondParameterField).toBeDefined()
fireEvent.change(secondParameterField, {
target: { value: "1" },
})
fireEvent.submit(secondParameter)
const validationError = await screen.findByText(
validationNumberNotIncreasing,
)
expect(validationError).toBeDefined()
})
it("rich parameter: number is not monotonically decreasing", async () => {
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValueOnce([
MockTemplateVersionParameter1,
MockTemplateVersionParameter5,
])
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValueOnce([
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter5,
])
renderWorkspaceBuildParametersPage()
const element = await screen.findByText(pageTitleText)
expect(element).toBeDefined()
const secondParameter = await screen.findByText(
MockTemplateVersionParameter5.description,
)
expect(secondParameter).toBeDefined()
const secondParameterField = await screen.findByLabelText(
MockTemplateVersionParameter5.name,
)
expect(secondParameterField).toBeDefined()
fireEvent.change(secondParameterField, {
target: { value: "6" },
})
fireEvent.submit(secondParameter)
const validationError = await screen.findByText(
validationNumberNotDecreasing,
)
expect(validationError).toBeDefined()
})
})
@@ -64,6 +64,7 @@ export const WorkspaceBuildParametersPageView: FC<
rich_parameter_values: ValidationSchemaForRichParameters(
"workspaceBuildParametersPage",
props.templateParameters,
initialRichParameterValues,
),
}),
enableReinitialize: true,
+20 -12
View File
@@ -672,10 +672,6 @@ export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter =
mutable: true,
icon: "/icon/folder.svg",
options: [],
validation_error: "",
validation_regex: "",
validation_min: 0,
validation_max: 0,
}
export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter =
@@ -687,10 +683,9 @@ export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter =
mutable: true,
icon: "/icon/folder.svg",
options: [],
validation_error: "",
validation_regex: "",
validation_min: 1,
validation_max: 3,
validation_monotonic: "increasing",
}
export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter =
@@ -704,8 +699,6 @@ export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter =
options: [],
validation_error: "No way!",
validation_regex: "^[a-z]{3}$",
validation_min: 0,
validation_max: 0,
}
export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter =
@@ -717,10 +710,20 @@ export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter =
mutable: false,
icon: "/icon/database.svg",
options: [],
validation_error: "",
validation_regex: "",
validation_min: 0,
validation_max: 0,
}
export const MockTemplateVersionParameter5: TypesGen.TemplateVersionParameter =
{
name: "fifth_parameter",
type: "number",
description: "This is fifth parameter",
default_value: "5",
mutable: true,
icon: "/icon/folder.svg",
options: [],
validation_min: 1,
validation_max: 10,
validation_monotonic: "decreasing",
}
// requests the MockWorkspace
@@ -1313,6 +1316,11 @@ export const MockWorkspaceBuildParameter2: TypesGen.WorkspaceBuildParameter = {
value: "3",
}
export const MockWorkspaceBuildParameter5: TypesGen.WorkspaceBuildParameter = {
name: MockTemplateVersionParameter5.name,
value: "5",
}
export const mockParameterSchema = (
partial: Partial<TypesGen.ParameterSchema>,
): TypesGen.ParameterSchema => {