@rancher/shell 3.0.12-rc.2 → 3.0.12-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apis/impl/apis.ts +6 -0
- package/apis/index.ts +26 -0
- package/apis/intf/resources-api/cluster-api.ts +18 -0
- package/apis/intf/resources-api/mgmt-api.ts +15 -0
- package/apis/intf/resources-api/resource-base.ts +107 -0
- package/apis/intf/resources-api/resource-constants.ts +147 -0
- package/apis/intf/resources-api/resources-api.ts +143 -0
- package/apis/intf/resources.ts +49 -0
- package/apis/intf/{modal.ts → shell-api/modal.ts} +21 -26
- package/apis/intf/shell-api/proxy.ts +216 -0
- package/apis/intf/{slide-in.ts → shell-api/slide-in.ts} +4 -3
- package/apis/intf/{system.ts → shell-api/system.ts} +4 -1
- package/apis/intf/shell.ts +12 -6
- package/apis/resources/__tests__/resources-api-class.test.ts +550 -0
- package/apis/resources/index.ts +22 -0
- package/apis/resources/resources-api-class.ts +187 -0
- package/apis/shell/__tests__/proxy.test.ts +369 -0
- package/apis/shell/index.ts +8 -1
- package/apis/shell/modal.ts +4 -1
- package/apis/shell/notifications.ts +9 -6
- package/apis/shell/proxy.ts +256 -0
- package/apis/shell/slide-in.ts +4 -1
- package/apis/vue-shim.d.ts +2 -1
- package/assets/data/aws-regions.json +4 -0
- package/assets/fonts/lato/LatoLatin-Black.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Black.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-BlackItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-BlackItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Bold.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Bold.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-BoldItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-BoldItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Heavy.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Heavy.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-HeavyItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-HeavyItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Italic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Italic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Light.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Light.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-LightItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-LightItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Medium.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Medium.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-MediumItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-MediumItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Regular.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Regular.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Semibold.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Semibold.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff2 +0 -0
- package/assets/styles/base/_variables.scss +2 -0
- package/assets/styles/fonts/_fontstack.scss +132 -8
- package/assets/translations/en-us.yaml +22 -5
- package/chart/monitoring/index.vue +10 -1
- package/components/ActionDropdownShell.vue +2 -1
- package/components/CruResourceFooter.vue +9 -5
- package/components/ExplorerProjectsNamespaces.vue +1 -1
- package/components/InstallHelmCharts.vue +2 -2
- package/components/LandingPagePreference.vue +14 -5
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +15 -1
- package/components/Resource/Detail/Metadata/index.vue +6 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +12 -1
- package/components/Resource/Detail/SpacedRow.vue +3 -1
- package/components/Resource/Detail/TitleBar/index.vue +10 -11
- package/components/ResourceList/Masthead.vue +12 -8
- package/components/SelectIconGrid.vue +0 -10
- package/components/SingleClusterInfo.vue +1 -0
- package/components/SortableTable/__tests__/sorting.test.ts +126 -0
- package/components/SortableTable/index.vue +6 -9
- package/components/SortableTable/selection.js +23 -5
- package/components/SortableTable/sorting.js +6 -3
- package/components/Wizard.vue +14 -13
- package/components/fleet/FleetBundles.vue +100 -12
- package/components/fleet/FleetClusterTargets/index.vue +37 -15
- package/components/fleet/__tests__/FleetClusterTargets.test.ts +149 -115
- package/components/fleet/__tests__/FleetClusters.test.ts +12 -12
- package/components/form/LabeledSelect.vue +20 -3
- package/components/form/NameNsDescription.vue +11 -0
- package/components/form/Security.vue +6 -2
- package/components/form/WorkloadPorts.vue +2 -7
- package/components/form/__tests__/Security.test.ts +76 -0
- package/components/formatter/Autoscaler.vue +4 -4
- package/components/formatter/ClusterKubeVersion.vue +27 -0
- package/components/formatter/ClusterLink.vue +1 -7
- package/components/formatter/ClusterProvider.vue +6 -10
- package/components/formatter/FleetSummaryGraph.vue +0 -3
- package/components/formatter/MachineSummaryGraph.vue +1 -1
- package/components/formatter/PodsUsage.vue +2 -2
- package/components/formatter/__tests__/Autoscaler.test.ts +19 -22
- package/components/formatter/__tests__/FleetSummaryGraph.test.ts +216 -0
- package/components/formatter/__tests__/PodsUsage.test.ts +6 -10
- package/components/nav/NamespaceFilter.vue +2 -2
- package/components/nav/TopLevelMenu.helper.ts +15 -3
- package/components/nav/TopLevelMenu.vue +16 -5
- package/components/nav/__tests__/TopLevelMenu.test.ts +145 -21
- package/components/templates/home.vue +18 -0
- package/components/templates/plain.vue +18 -0
- package/components/templates/standalone.vue +17 -0
- package/composables/useFormValidation.ts +93 -0
- package/composables/useVeeValidateField.test.ts +159 -0
- package/composables/useVeeValidateField.ts +67 -0
- package/config/pagination-table-headers.js +18 -1
- package/config/product/manager.js +82 -21
- package/config/router/routes.js +6 -0
- package/config/table-headers.js +20 -1
- package/config/types.js +2 -1
- package/core/__tests__/plugin-products.test.ts +904 -20
- package/core/plugin-products-base.ts +107 -7
- package/core/plugin-products.ts +4 -0
- package/core/plugin-types.ts +111 -1
- package/core/plugin.ts +15 -7
- package/core/productDebugger.js +9 -4
- package/core/types-provisioning.ts +43 -30
- package/core/types.ts +57 -20
- package/detail/__tests__/pod.test.ts +41 -0
- package/detail/harvesterhci.io.management.cluster.vue +6 -2
- package/detail/pod.vue +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +4 -10
- package/edit/auth/__tests__/azuread.test.ts +217 -34
- package/edit/auth/azuread.vue +122 -14
- package/edit/auth/oidc.vue +2 -2
- package/edit/networking.k8s.io.ingress/DefaultBackend.vue +13 -4
- package/edit/networking.k8s.io.ingress/RulePath.vue +8 -4
- package/edit/networking.k8s.io.ingress/index.vue +75 -20
- package/edit/provisioning.cattle.io.cluster/__tests__/MachinePool.test.ts +104 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +11 -7
- package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -4
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +37 -4
- package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +132 -7
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -1
- package/edit/secret/__tests__/ssh.test.ts +5 -6
- package/edit/secret/basic.vue +31 -0
- package/edit/secret/index.vue +68 -17
- package/edit/secret/registry.vue +38 -0
- package/edit/secret/ssh.vue +29 -0
- package/edit/secret/tls.vue +30 -0
- package/edit/service.vue +4 -4
- package/edit/workload/Upgrading.vue +3 -3
- package/edit/workload/__tests__/Upgrading.test.ts +6 -9
- package/edit/workload/mixins/workload.js +2 -1
- package/list/fleet.cattle.io.bundle.vue +7 -104
- package/list/fleet.cattle.io.clusterregistrationtoken.vue +20 -0
- package/list/provisioning.cattle.io.cluster.vue +262 -180
- package/list/utils/management.cattle.io.cluster.utils.ts +128 -0
- package/mixins/__tests__/chart.test.ts +112 -0
- package/mixins/brand.js +2 -1
- package/mixins/chart.js +12 -8
- package/mixins/resource-fetch-api-pagination.js +41 -5
- package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +67 -67
- package/models/__tests__/management.cattle.io.cluster.test.ts +1 -1
- package/models/__tests__/management.cattle.io.node.ts +6 -5
- package/models/__tests__/management.cattle.io.nodepool.ts +5 -4
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +32 -11
- package/models/base-cluster.x-k8s.io.js +26 -0
- package/models/cluster.js +1 -1
- package/models/cluster.x-k8s.io.machine.js +4 -22
- package/models/cluster.x-k8s.io.machinedeployment.js +2 -20
- package/models/cluster.x-k8s.io.machineset.js +2 -20
- package/models/compliance.cattle.io.clusterscan.js +130 -2
- package/models/ext.cattle.io.kubeconfig.ts +4 -7
- package/models/fleet-application.js +3 -1
- package/models/management.cattle.io.cluster.js +417 -40
- package/models/management.cattle.io.node.js +6 -4
- package/models/management.cattle.io.nodepool.js +1 -1
- package/models/networking.k8s.io.ingress.js +12 -4
- package/models/provisioning.cattle.io.cluster.js +47 -330
- package/models/rke.cattle.io.etcdsnapshot.js +1 -2
- package/package.json +11 -29
- package/pages/__tests__/readme.test.ts +49 -0
- package/pages/auth/setup.vue +2 -3
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +76 -0
- package/pages/c/_cluster/apps/charts/chart.vue +60 -8
- package/pages/c/_cluster/apps/charts/install.vue +10 -7
- package/pages/c/_cluster/explorer/__tests__/index.test.ts +23 -25
- package/pages/c/_cluster/explorer/index.vue +5 -49
- package/pages/c/_cluster/istio/__tests__/istio.index.test.ts +194 -0
- package/pages/c/_cluster/istio/index.vue +21 -6
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -0
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +719 -2
- package/pages/c/_cluster/uiplugins/index.vue +203 -197
- package/pages/diagnostic.vue +13 -17
- package/pages/fail-whale.vue +18 -0
- package/pages/home.vue +77 -260
- package/pages/readme.vue +88 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +88 -0
- package/plugins/dashboard-store/actions.js +40 -18
- package/plugins/dashboard-store/resource-class.js +5 -2
- package/plugins/steve/__tests__/subscribe.spec.ts +6 -3
- package/plugins/steve/steve-pagination-utils.ts +11 -3
- package/plugins/steve/subscribe.js +35 -5
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +10 -4
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +7 -52
- package/rancher-components/RcButton/RcButton.test.ts +37 -1
- package/rancher-components/RcButton/RcButton.vue +38 -8
- package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -8
- package/store/__tests__/catalog.test.ts +115 -1
- package/store/__tests__/type-map.test.ts +556 -1
- package/store/action-menu.js +8 -3
- package/store/auth.js +1 -1
- package/store/aws.js +27 -16
- package/store/catalog.js +27 -3
- package/store/digitalocean.js +20 -38
- package/store/index.js +2 -0
- package/store/linode.js +25 -40
- package/store/pnap.js +1 -0
- package/store/type-map.js +111 -29
- package/tsconfig.paths.json +8 -8
- package/types/kube/kube-api.ts +14 -1
- package/types/rancher/steve.api.ts +12 -12
- package/types/resources/settings.d.ts +2 -1
- package/types/shell/index.d.ts +102 -2
- package/types/store/dashboard-store.types.ts +108 -11
- package/types/store/pagination.types.ts +6 -3
- package/utils/__tests__/alertmanagerconfig.test.ts +117 -0
- package/utils/__tests__/async.test.ts +87 -0
- package/utils/__tests__/aws.test.ts +140 -0
- package/utils/__tests__/banners.test.ts +176 -0
- package/utils/__tests__/chart.test.ts +64 -1
- package/utils/__tests__/color.test.ts +226 -0
- package/utils/__tests__/duration.test.ts +140 -0
- package/utils/__tests__/fleet.test.ts +340 -0
- package/utils/__tests__/ingress.test.ts +553 -0
- package/utils/__tests__/kube.test.ts +68 -0
- package/utils/__tests__/namespace-filter.test.ts +109 -0
- package/utils/__tests__/pagination-utils.test.ts +361 -0
- package/utils/__tests__/parse-externalid.test.ts +137 -0
- package/utils/__tests__/perf-setting.utils.test.ts +98 -0
- package/utils/__tests__/poller-sequential.test.ts +177 -0
- package/utils/__tests__/poller.test.ts +170 -0
- package/utils/__tests__/promise.test.ts +346 -0
- package/utils/__tests__/settings.test.ts +140 -0
- package/utils/__tests__/sort-utils.test.ts +301 -0
- package/utils/__tests__/string-utils.test.ts +798 -0
- package/utils/__tests__/string.test.ts +23 -1
- package/utils/__tests__/style.test.ts +154 -0
- package/utils/__tests__/svg-filter.test.ts +184 -0
- package/utils/__tests__/units.test.ts +417 -0
- package/utils/__tests__/versions.test.ts +128 -0
- package/utils/__tests__/xccdf.test.ts +391 -0
- package/utils/chart.js +36 -0
- package/utils/fleet.ts +13 -3
- package/utils/gatekeeper/__tests__/util.test.ts +174 -0
- package/utils/gc/__tests__/gc-interval.test.ts +119 -0
- package/utils/gc/__tests__/gc-root-store.test.ts +225 -0
- package/utils/gc/__tests__/gc-route-changed.test.ts +96 -0
- package/utils/gc/__tests__/gc.test.ts +487 -0
- package/utils/ingress.ts +9 -1
- package/utils/pagination-utils.ts +2 -1
- package/utils/string.js +25 -2
- package/utils/uiplugins.ts +5 -5
- package/utils/validators/__tests__/cluster-name.test.ts +110 -0
- package/utils/validators/__tests__/cron-schedule.test.ts +79 -0
- package/utils/validators/__tests__/index.test.ts +481 -0
- package/utils/validators/__tests__/kubernetes-name.test.ts +163 -0
- package/utils/validators/__tests__/misc-validators.test.ts +246 -0
- package/utils/validators/__tests__/pod-affinity.test.ts +382 -0
- package/utils/validators/__tests__/prometheusrule.test.ts +211 -0
- package/utils/validators/__tests__/role-template.test.ts +149 -0
- package/utils/validators/__tests__/service.test.ts +283 -0
- package/utils/validators/__tests__/setting.test.js +32 -0
- package/utils/validators/formRules/__tests__/index.test.ts +50 -0
- package/utils/validators/formRules/index.ts +5 -5
- package/utils/validators/machine-pool.ts +1 -1
- package/utils/validators/setting.js +18 -3
- package/utils/xccdf.ts +418 -0
- package/assets/fonts/lato/lato-v17-latin-700.woff +0 -0
- package/assets/fonts/lato/lato-v17-latin-700.woff2 +0 -0
- package/assets/fonts/lato/lato-v17-latin-regular.woff +0 -0
- package/assets/fonts/lato/lato-v17-latin-regular.woff2 +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import cronstrue from 'cronstrue';
|
|
2
|
+
import { cronSchedule, cronScheduleRule } from '@shell/utils/validators/cron-schedule';
|
|
3
|
+
|
|
4
|
+
jest.mock('cronstrue', () => ({ toString: jest.fn() }));
|
|
5
|
+
|
|
6
|
+
const mockCronstrue = cronstrue as jest.Mocked<typeof cronstrue>;
|
|
7
|
+
const mockGetters = { 'i18n/t': (key: string) => key };
|
|
8
|
+
|
|
9
|
+
describe('cronScheduleRule', () => {
|
|
10
|
+
it('calls cronstrue.toString with verbose:true', () => {
|
|
11
|
+
mockCronstrue.toString.mockReturnValue('every minute');
|
|
12
|
+
cronScheduleRule.validation('* * * * *');
|
|
13
|
+
|
|
14
|
+
expect(mockCronstrue.toString).toHaveBeenCalledWith('* * * * *', { verbose: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('has the correct message key', () => {
|
|
18
|
+
expect(cronScheduleRule.message).toStrictEqual('validation.invalidCron');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('cronSchedule', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does not add an error for a valid cron expression', () => {
|
|
28
|
+
mockCronstrue.toString.mockReturnValue('every minute');
|
|
29
|
+
const errors: string[] = [];
|
|
30
|
+
|
|
31
|
+
cronSchedule('* * * * *', mockGetters, errors);
|
|
32
|
+
|
|
33
|
+
expect(errors).toStrictEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('adds an error for an invalid cron expression', () => {
|
|
37
|
+
mockCronstrue.toString.mockImplementation(() => {
|
|
38
|
+
throw new Error('Invalid cron expression');
|
|
39
|
+
});
|
|
40
|
+
const errors: string[] = [];
|
|
41
|
+
|
|
42
|
+
cronSchedule('not-a-cron', mockGetters, errors);
|
|
43
|
+
|
|
44
|
+
expect(errors).toStrictEqual(['validation.invalidCron']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('uses default empty string for schedule when not provided', () => {
|
|
48
|
+
mockCronstrue.toString.mockImplementation(() => {
|
|
49
|
+
throw new Error('Invalid cron expression');
|
|
50
|
+
});
|
|
51
|
+
const errors: string[] = [];
|
|
52
|
+
|
|
53
|
+
cronSchedule(undefined as any, mockGetters, errors);
|
|
54
|
+
|
|
55
|
+
expect(errors).toStrictEqual(['validation.invalidCron']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not push multiple errors for a single invalid schedule', () => {
|
|
59
|
+
mockCronstrue.toString.mockImplementation(() => {
|
|
60
|
+
throw new Error('Invalid');
|
|
61
|
+
});
|
|
62
|
+
const errors: string[] = [];
|
|
63
|
+
|
|
64
|
+
cronSchedule('bad', mockGetters, errors);
|
|
65
|
+
|
|
66
|
+
expect(errors).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('preserves existing errors when adding a new one', () => {
|
|
70
|
+
mockCronstrue.toString.mockImplementation(() => {
|
|
71
|
+
throw new Error('Invalid');
|
|
72
|
+
});
|
|
73
|
+
const errors = ['existing error'];
|
|
74
|
+
|
|
75
|
+
cronSchedule('bad', mockGetters, errors);
|
|
76
|
+
|
|
77
|
+
expect(errors).toStrictEqual(['existing error', 'validation.invalidCron']);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import {
|
|
2
|
+
displayKeyFor,
|
|
3
|
+
validateLength,
|
|
4
|
+
validateChars,
|
|
5
|
+
validateHostname,
|
|
6
|
+
validateDnsLabel,
|
|
7
|
+
validateDnsLikeTypes,
|
|
8
|
+
validateBoolean,
|
|
9
|
+
} from '@shell/utils/validators';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Simple i18n getters mock: i18n/t returns a formatted key string,
|
|
13
|
+
* i18n/exists returns false by default (can be overridden per-test).
|
|
14
|
+
*/
|
|
15
|
+
function makeGetters(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
16
|
+
return {
|
|
17
|
+
'i18n/t': (key: string, args?: unknown) => (args ? `${ key }:${ JSON.stringify(args) }` : key),
|
|
18
|
+
'i18n/exists': () => false,
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('displayKeyFor', () => {
|
|
24
|
+
it('returns i18n label when model label key exists', () => {
|
|
25
|
+
const getters = makeGetters({
|
|
26
|
+
'i18n/exists': (key: string) => key === 'model.Foo.bar.label',
|
|
27
|
+
'i18n/t': (key: string) => `translated:${ key }`,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(displayKeyFor('Foo', 'bar', getters)).toBe('translated:model.Foo.bar.label');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns i18n prefix translation when prefix exists but not label', () => {
|
|
34
|
+
const getters = makeGetters({
|
|
35
|
+
'i18n/exists': (key: string) => key === 'model.Foo.bar',
|
|
36
|
+
'i18n/t': (key: string) => `translated:${ key }`,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(displayKeyFor('Foo', 'bar', getters)).toBe('translated:model.Foo.bar');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('strips Id suffix and camelToTitle when key ends with Id', () => {
|
|
43
|
+
const getters = makeGetters();
|
|
44
|
+
|
|
45
|
+
expect(displayKeyFor('Foo', 'clusterGroupId', getters)).toBe('Cluster Group');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('falls back to camelToTitle for plain keys', () => {
|
|
49
|
+
const getters = makeGetters();
|
|
50
|
+
|
|
51
|
+
expect(displayKeyFor('Foo', 'fooBar', getters)).toBe('Foo Bar');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('validateLength', () => {
|
|
56
|
+
describe('required field validation', () => {
|
|
57
|
+
it('pushes required error for undefined value when required=true and nullable=false', () => {
|
|
58
|
+
const getters = makeGetters();
|
|
59
|
+
const errors = validateLength(undefined, { required: true, nullable: false }, 'Name', getters);
|
|
60
|
+
|
|
61
|
+
expect(errors).toStrictEqual(['validation.required:{"key":"Name"}']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('pushes required error for empty string when required=true', () => {
|
|
65
|
+
const getters = makeGetters();
|
|
66
|
+
const errors = validateLength('', { required: true, nullable: false }, 'Name', getters);
|
|
67
|
+
|
|
68
|
+
expect(errors).toStrictEqual(['validation.required:{"key":"Name"}']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('pushes required error for empty object when required=true', () => {
|
|
72
|
+
const getters = makeGetters();
|
|
73
|
+
const errors = validateLength({}, { required: true, nullable: false }, 'Name', getters);
|
|
74
|
+
|
|
75
|
+
expect(errors).toStrictEqual(['validation.required:{"key":"Name"}']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not push required error for 0 when required=true (0 is a valid falsy number)', () => {
|
|
79
|
+
const getters = makeGetters();
|
|
80
|
+
const errors = validateLength(0, { required: true, nullable: false }, 'Name', getters);
|
|
81
|
+
|
|
82
|
+
expect(errors).toStrictEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns early without length check when required error is pushed', () => {
|
|
86
|
+
const getters = makeGetters();
|
|
87
|
+
const errors = validateLength(undefined, {
|
|
88
|
+
required: true, nullable: false, minLength: 5, maxLength: 10
|
|
89
|
+
}, 'Name', getters);
|
|
90
|
+
|
|
91
|
+
expect(errors).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('does not push error for null when nullable=true', () => {
|
|
95
|
+
const getters = makeGetters();
|
|
96
|
+
const errors = validateLength(null, { required: true, nullable: true }, 'Name', getters);
|
|
97
|
+
|
|
98
|
+
expect(errors).toStrictEqual([]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('string length validation', () => {
|
|
103
|
+
it.each([
|
|
104
|
+
{
|
|
105
|
+
desc: 'exactly error when min===max and string length is wrong',
|
|
106
|
+
val: 'ab',
|
|
107
|
+
field: { minLength: 5, maxLength: 5 },
|
|
108
|
+
key: 'Name',
|
|
109
|
+
expected: 'validation.stringLength.exactly',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
desc: 'between error when min!==max and string length is out of range',
|
|
113
|
+
val: 'ab',
|
|
114
|
+
field: { minLength: 5, maxLength: 10 },
|
|
115
|
+
key: 'Name',
|
|
116
|
+
expected: 'validation.stringLength.between',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
desc: 'min error when only min is set and string is too short',
|
|
120
|
+
val: 'ab',
|
|
121
|
+
field: { minLength: 5 },
|
|
122
|
+
key: 'Name',
|
|
123
|
+
expected: 'validation.stringLength.min',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
desc: 'max error when only max is set and string is too long',
|
|
127
|
+
val: 'abcdefghij',
|
|
128
|
+
field: { maxLength: 5 },
|
|
129
|
+
key: 'Name',
|
|
130
|
+
expected: 'validation.stringLength.max',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
desc: 'arrayLength key for array[ type',
|
|
134
|
+
val: [],
|
|
135
|
+
field: { type: 'array[string]', minLength: 1 },
|
|
136
|
+
key: 'Items',
|
|
137
|
+
expected: 'validation.arrayLength.min',
|
|
138
|
+
},
|
|
139
|
+
])('pushes $desc', ({
|
|
140
|
+
val, field, key, expected,
|
|
141
|
+
}) => {
|
|
142
|
+
const getters = makeGetters();
|
|
143
|
+
const errors = validateLength(val, field, key, getters);
|
|
144
|
+
|
|
145
|
+
expect(errors[0]).toContain(expected);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns no errors when string is within min/max bounds', () => {
|
|
149
|
+
const getters = makeGetters();
|
|
150
|
+
const errors = validateLength('abc', { minLength: 2, maxLength: 5 }, 'Name', getters);
|
|
151
|
+
|
|
152
|
+
expect(errors).toStrictEqual([]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('number min/max validation', () => {
|
|
157
|
+
it.each([
|
|
158
|
+
{
|
|
159
|
+
desc: 'number.exactly when min===max and value is out of range',
|
|
160
|
+
val: 3,
|
|
161
|
+
field: { min: 5, max: 5 },
|
|
162
|
+
expected: 'validation.number.exactly',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
desc: 'number.between when min!==max and value is out of range',
|
|
166
|
+
val: 3,
|
|
167
|
+
field: { min: 5, max: 10 },
|
|
168
|
+
expected: 'validation.number.between',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
desc: 'number.min when only min is set and value is below min',
|
|
172
|
+
val: 2,
|
|
173
|
+
field: { min: 5 },
|
|
174
|
+
expected: 'validation.number.min',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
desc: 'number.max when only max is set and value exceeds max',
|
|
178
|
+
val: 20,
|
|
179
|
+
field: { max: 10 },
|
|
180
|
+
expected: 'validation.number.max',
|
|
181
|
+
},
|
|
182
|
+
])('pushes $desc', ({ val, field, expected }) => {
|
|
183
|
+
const getters = makeGetters();
|
|
184
|
+
const errors = validateLength(val, field, 'Count', getters);
|
|
185
|
+
|
|
186
|
+
expect(errors[0]).toContain(expected);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns no errors when number is within range', () => {
|
|
190
|
+
const getters = makeGetters();
|
|
191
|
+
const errors = validateLength(7, { min: 5, max: 10 }, 'Count', getters);
|
|
192
|
+
|
|
193
|
+
expect(errors).toStrictEqual([]);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('validateChars', () => {
|
|
199
|
+
it('pushes chars error when value contains invalid chars defined by validChars', () => {
|
|
200
|
+
const getters = makeGetters();
|
|
201
|
+
const errors = validateChars('hello!', { validChars: 'a-z' }, 'Name', getters);
|
|
202
|
+
|
|
203
|
+
expect(errors[0]).toContain('validation.chars');
|
|
204
|
+
expect(errors[0]).toContain('!');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('pushes chars error when value contains chars in invalidChars', () => {
|
|
208
|
+
const getters = makeGetters();
|
|
209
|
+
const errors = validateChars('hello world', { invalidChars: ' ' }, 'Name', getters);
|
|
210
|
+
|
|
211
|
+
expect(errors[0]).toContain('validation.chars');
|
|
212
|
+
expect(errors[0]).toContain('[space]');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('converts space character to [space] in the error message', () => {
|
|
216
|
+
const getters = makeGetters();
|
|
217
|
+
const errors = validateChars('foo bar', { invalidChars: ' ' }, 'Name', getters);
|
|
218
|
+
|
|
219
|
+
expect(errors[0]).toContain('[space]');
|
|
220
|
+
expect(errors[0]).not.toContain('" "');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('deduplicates repeated invalid chars in error message', () => {
|
|
224
|
+
const getters = makeGetters();
|
|
225
|
+
const errors = validateChars('a!b!c!', { validChars: 'a-z' }, 'Name', getters);
|
|
226
|
+
|
|
227
|
+
// should only contain one unique '!' in the chars list
|
|
228
|
+
const charsArg = errors[0];
|
|
229
|
+
|
|
230
|
+
expect((charsArg.match(/!/g) || [])).toHaveLength(1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('returns no errors when value matches validChars', () => {
|
|
234
|
+
const getters = makeGetters();
|
|
235
|
+
const errors = validateChars('hello', { validChars: 'a-z' }, 'Name', getters);
|
|
236
|
+
|
|
237
|
+
expect(errors).toStrictEqual([]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns no errors when no validChars or invalidChars defined', () => {
|
|
241
|
+
const getters = makeGetters();
|
|
242
|
+
const errors = validateChars('anything!@#', {}, 'Name', getters);
|
|
243
|
+
|
|
244
|
+
expect(errors).toStrictEqual([]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('validateHostname', () => {
|
|
249
|
+
it.each([
|
|
250
|
+
{
|
|
251
|
+
desc: 'startDot when hostname starts with a dot',
|
|
252
|
+
val: '.example.com',
|
|
253
|
+
opts: {},
|
|
254
|
+
expected: 'startDot',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
desc: 'empty when hostname is empty string',
|
|
258
|
+
val: '',
|
|
259
|
+
opts: {},
|
|
260
|
+
expected: 'empty',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
desc: 'tooLong when hostname exceeds 253 characters',
|
|
264
|
+
val: 'a'.repeat(254),
|
|
265
|
+
opts: {},
|
|
266
|
+
expected: 'tooLong',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
desc: 'tooLong with custom max length option',
|
|
270
|
+
val: 'abcdef',
|
|
271
|
+
opts: { max: 5 },
|
|
272
|
+
expected: 'tooLong',
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
desc: 'endDot in restricted mode when hostname ends with dot',
|
|
276
|
+
val: 'example.com.',
|
|
277
|
+
opts: { restricted: true },
|
|
278
|
+
expected: 'endDot',
|
|
279
|
+
},
|
|
280
|
+
])('pushes $desc', ({ val, opts, expected }) => {
|
|
281
|
+
const getters = makeGetters();
|
|
282
|
+
const errors = validateHostname(val, 'Host', getters, opts);
|
|
283
|
+
|
|
284
|
+
expect(errors.some((e: string) => e.includes(expected))).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it.each([
|
|
288
|
+
{
|
|
289
|
+
desc: 'trailing dot in non-restricted mode (FQDN notation)',
|
|
290
|
+
val: 'example.com.',
|
|
291
|
+
opts: { restricted: false },
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
desc: 'a valid simple hostname',
|
|
295
|
+
val: 'my-host',
|
|
296
|
+
opts: {},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
desc: 'a valid FQDN',
|
|
300
|
+
val: 'my-host.example.com',
|
|
301
|
+
opts: {},
|
|
302
|
+
},
|
|
303
|
+
])('allows $desc', ({ val, opts }) => {
|
|
304
|
+
const getters = makeGetters();
|
|
305
|
+
const errors = validateHostname(val, 'Host', getters, opts);
|
|
306
|
+
|
|
307
|
+
expect(errors).toStrictEqual([]);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('validateDnsLabel', () => {
|
|
312
|
+
it.each([
|
|
313
|
+
{
|
|
314
|
+
desc: 'startHyphen when label starts with a hyphen',
|
|
315
|
+
label: '-bad',
|
|
316
|
+
opts: {},
|
|
317
|
+
expected: 'startHyphen',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
desc: 'endHyphen when label ends with a hyphen',
|
|
321
|
+
label: 'bad-',
|
|
322
|
+
opts: {},
|
|
323
|
+
expected: 'endHyphen',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
desc: 'emptyLabel when label is empty',
|
|
327
|
+
label: '',
|
|
328
|
+
opts: {},
|
|
329
|
+
expected: 'emptyLabel',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
desc: 'tooLongLabel when label exceeds 63 characters',
|
|
333
|
+
label: 'a'.repeat(64),
|
|
334
|
+
opts: {},
|
|
335
|
+
expected: 'tooLongLabel',
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
desc: 'startNumber in restricted mode when label starts with a digit',
|
|
339
|
+
label: '1abc',
|
|
340
|
+
opts: { restricted: true },
|
|
341
|
+
expected: 'startNumber',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
desc: 'doubleHyphen when label has -- at 3rd and 4th position (non-xn prefix)',
|
|
345
|
+
label: 'ab--cd',
|
|
346
|
+
opts: {},
|
|
347
|
+
expected: 'doubleHyphen',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
desc: 'doubleHyphen for ianaServiceName with any consecutive hyphens',
|
|
351
|
+
label: 'xn--valid',
|
|
352
|
+
opts: { ianaServiceName: true },
|
|
353
|
+
expected: 'doubleHyphen',
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
desc: 'hostname.startHyphen when forHostname=true',
|
|
357
|
+
label: '-bad',
|
|
358
|
+
opts: { forHostname: true },
|
|
359
|
+
expected: 'hostname.startHyphen',
|
|
360
|
+
},
|
|
361
|
+
])('pushes $desc', ({ label, opts, expected }) => {
|
|
362
|
+
const getters = makeGetters();
|
|
363
|
+
const errors = validateDnsLabel(label, 'Label', getters, opts);
|
|
364
|
+
|
|
365
|
+
expect(errors.some((e: string) => e.includes(expected))).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it.each([
|
|
369
|
+
{
|
|
370
|
+
desc: 'a valid DNS label',
|
|
371
|
+
label: 'my-label',
|
|
372
|
+
opts: {},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
desc: 'digit-start in non-restricted mode',
|
|
376
|
+
label: '1abc',
|
|
377
|
+
opts: { restricted: false },
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
desc: 'xn-- prefix (IDN string)',
|
|
381
|
+
label: 'xn--nxasmq6b',
|
|
382
|
+
opts: {},
|
|
383
|
+
},
|
|
384
|
+
])('allows $desc without errors', ({ label, opts }) => {
|
|
385
|
+
const getters = makeGetters();
|
|
386
|
+
const errors = validateDnsLabel(label, 'Label', getters, opts);
|
|
387
|
+
|
|
388
|
+
expect(errors).toStrictEqual([]);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('validateDnsLikeTypes', () => {
|
|
393
|
+
it.each([
|
|
394
|
+
{
|
|
395
|
+
desc: 'dnsLabel type pushes error for empty label',
|
|
396
|
+
val: '',
|
|
397
|
+
type: 'dnsLabel',
|
|
398
|
+
expected: 'emptyLabel',
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
desc: 'dnsLabelRestricted type pushes startNumber for digit-start',
|
|
402
|
+
val: '1abc',
|
|
403
|
+
type: 'dnsLabelRestricted',
|
|
404
|
+
expected: 'startNumber',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
desc: 'hostname type pushes startDot for dot-start',
|
|
408
|
+
val: '.bad',
|
|
409
|
+
type: 'hostname',
|
|
410
|
+
expected: 'startDot',
|
|
411
|
+
},
|
|
412
|
+
])('validates $desc', ({ val, type, expected }) => {
|
|
413
|
+
const getters = makeGetters();
|
|
414
|
+
const errors = validateDnsLikeTypes(val, type, 'Name', getters, {});
|
|
415
|
+
|
|
416
|
+
expect(errors.some((e: string) => e.includes(expected))).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('returns no errors for unknown type (no-op)', () => {
|
|
420
|
+
const getters = makeGetters();
|
|
421
|
+
const errors = validateDnsLikeTypes('anything', 'unknownType', 'Name', getters, {});
|
|
422
|
+
|
|
423
|
+
expect(errors).toStrictEqual([]);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe('validateBoolean', () => {
|
|
428
|
+
it('pushes required error when field is required and value is undefined', () => {
|
|
429
|
+
const getters = makeGetters();
|
|
430
|
+
const errors: string[] = [];
|
|
431
|
+
|
|
432
|
+
validateBoolean(undefined, { required: true }, 'Flag', getters, errors);
|
|
433
|
+
|
|
434
|
+
expect(errors.some((e: string) => e.includes('validation.required'))).toBe(true);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('does not push required error when value is false (explicitly set)', () => {
|
|
438
|
+
const getters = makeGetters();
|
|
439
|
+
const errors: string[] = [];
|
|
440
|
+
|
|
441
|
+
validateBoolean(false, { required: true }, 'Flag', getters, errors);
|
|
442
|
+
|
|
443
|
+
expect(errors).toStrictEqual([]);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('does not push required error when value is true', () => {
|
|
447
|
+
const getters = makeGetters();
|
|
448
|
+
const errors: string[] = [];
|
|
449
|
+
|
|
450
|
+
validateBoolean(true, { required: true }, 'Flag', getters, errors);
|
|
451
|
+
|
|
452
|
+
expect(errors).toStrictEqual([]);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('pushes boolean error when value is non-boolean truthy (e.g. string)', () => {
|
|
456
|
+
const getters = makeGetters();
|
|
457
|
+
const errors: string[] = [];
|
|
458
|
+
|
|
459
|
+
validateBoolean('yes', { required: false }, 'Flag', getters, errors);
|
|
460
|
+
|
|
461
|
+
expect(errors.some((e: string) => e.includes('validation.boolean'))).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('does not push error when field is not required and value is undefined', () => {
|
|
465
|
+
const getters = makeGetters();
|
|
466
|
+
const errors: string[] = [];
|
|
467
|
+
|
|
468
|
+
validateBoolean(undefined, { required: false }, 'Flag', getters, errors);
|
|
469
|
+
|
|
470
|
+
expect(errors).toStrictEqual([]);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('does not push boolean error for numeric 0 when required=false and val is falsy', () => {
|
|
474
|
+
const getters = makeGetters();
|
|
475
|
+
const errors: string[] = [];
|
|
476
|
+
|
|
477
|
+
validateBoolean(0, { required: false }, 'Flag', getters, errors);
|
|
478
|
+
|
|
479
|
+
expect(errors).toStrictEqual([]);
|
|
480
|
+
});
|
|
481
|
+
});
|