add entity link field validation & tests

This commit is contained in:
Andrew Thauer
2021-01-28 07:16:22 -05:00
parent 91fc991a79
commit a515fc1d11
7 changed files with 180 additions and 5 deletions
@@ -115,8 +115,98 @@ describe('FieldFormatEntityPolicy', () => {
await expect(policy.enforce(data)).rejects.toThrow(/tags.*"Hello World"/i);
});
it('rejects bad link icon value', async () => {
data.metadata.links.push({ url: 'https://foo', icon: 'some icon' });
await expect(policy.enforce(data)).rejects.toThrow(/links.*"some icon"/i);
it('accepts missing links', async () => {
delete data.metadata.links;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('accepts empty links array', async () => {
data.metadata.links = [];
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('accepts multiple links', async () => {
data.metadata.links = [{ url: 'http://foo' }, { url: 'https://bar' }];
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects missing link url value', async () => {
data.metadata.links = [{}];
await expect(policy.enforce(data)).rejects.toThrow(/links.0/);
});
it('rejects a single bad missing url value', async () => {
data.metadata.links = [{ url: 'http://good' }, { url: '' }];
await expect(policy.enforce(data)).rejects.toThrow(/links.1/);
});
it('rejects empty link url value', async () => {
data.metadata.links = [{ url: '' }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0/);
});
it('rejects bad link url value', async () => {
data.metadata.links = [{ url: 'invalid' }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0/);
});
it('accepts missing link title', async () => {
data.metadata.links = [{ url: 'http://foo', icon: 'dashboard' }];
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects empty link title', async () => {
data.metadata.links = [{ url: 'http://foo', title: '' }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0/);
});
it.each([[123], [{}], [[]]])(
'rejects bad link title %s',
async (title: unknown) => {
data.metadata.links = [{ url: 'http://foo', title }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0/);
},
);
it('rejects a single bad link title', async () => {
data.metadata.links = [
{ url: 'http://foo', title: 'good' },
{ url: 'http://foo', title: '' },
];
await expect(policy.enforce(data)).rejects.toThrow(/links.1/);
});
it('accepts missing link icon', async () => {
data.metadata.links = [{ url: 'http://foo', title: 'foo' }];
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects empty link icon', async () => {
data.metadata.links = [{ url: 'http://foo', icon: '' }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0/);
});
it.each([['dashboard'], ['admin-dashboard'], ['foo_dashboard']])(
'accepts valid link icon',
async icon => {
data.metadata.links = [{ url: 'http://foo', icon }];
await expect(policy.enforce(data)).resolves.toBe(data);
},
);
it.each([[123], [{}], [[]], ['abc xyz']])(
'rejects bad link icon value %s',
async (icon: unknown) => {
data.metadata.links = [{ url: 'http://foo', icon }];
await expect(policy.enforce(data)).rejects.toThrow(/links.0/);
},
);
it('rejects a single bad link icon value', async () => {
data.metadata.links = [
{ url: 'http://foo', icon: 'good' },
{ url: 'http://foo', icon: 'not good' },
];
await expect(policy.enforce(data)).rejects.toThrow(/links.1/);
});
});
@@ -83,6 +83,12 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
expectation =
'a string that is a sequence of [a-zA-Z][a-z0-9A-Z], at most 63 characters in total';
break;
case 'isValidUrl':
expectation = 'a string that is a valid url';
break;
case 'isValidString':
expectation = 'a non empty string';
break;
default:
expectation = undefined;
break;
@@ -137,7 +143,9 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
const links = entity.metadata.links ?? [];
for (let i = 0; i < links.length; ++i) {
optional(`links.${i}`, links[i]?.icon, this.validators.isValidEntityName);
require(`links.${i}`, links[i]?.url, this.validators.isValidLinkUrl);
optional(`links.${i}`, links[i]?.title, this.validators.isValidLinkTitle);
optional(`links.${i}`, links[i]?.icon, this.validators.isValidLinkIcon);
}
return entity;
@@ -214,7 +214,7 @@ describe('SchemaValidEntityPolicy', () => {
it.each([['invalid type'], [123], [{}], [{ url: 'https://foo' }]])(
'rejects bad links type %s',
async (val: any) => {
async (val: unknown) => {
data.metadata.links = val;
await expect(policy.enforce(data)).rejects.toThrow(/links/);
},
@@ -161,4 +161,44 @@ describe('CommonValidatorFunctions', () => {
])(`isValidDnsLabel %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isValidDnsLabel(value)).toBe(result);
});
it.each([
[null, false],
[7, false],
['', false],
['abc', false],
[' abc', false],
['', false],
[{}, false],
['http://foo', true],
['https://www.foo.com/', true],
['https://foo.com:8080', true],
['https://foo:8080/page', true],
['https://foo:8080/sub/page', true],
['https://foo:8080/sub/page?query', true],
['https://foo:8080/sub/page/?query=value', true],
['https://foo:8080/sub/page/?query=value&', true],
['https://foo:8080/sub/page/?query=value&another=val', true],
['https://foo.com/page#fragment', true],
['ftp://ftp.some.domain.com/path', true],
['xyz://custom-protocol:4444/path', true],
])(`isValidUrl %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isValidUrl(value)).toBe(result);
});
it.each([
[null, false],
[true, false],
[7, false],
[{}, false],
['', false],
[' ', false],
[' ', false],
['abc', true],
[' abc ', true],
['abc xyz', true],
['abc xyz abc.', true],
])(`isValidString %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isValidString(value)).toBe(result);
});
});
@@ -92,4 +92,35 @@ export class CommonValidatorFunctions {
/^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value)
);
}
/**
* Checks that the value is a valid URL.
*
* @param value The value to check
*/
static isValidUrl(value: unknown): boolean {
if (typeof value !== 'string') {
return false;
}
try {
const url = new URL(value);
return !!url;
} catch (_) {
return false;
}
}
/**
* Checks that the value is a non empty string value.
*
* @param value The value to check
*/
static isValidString(value: unknown): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
/^(?!\s*$).+/.test(value)
);
}
}
@@ -28,6 +28,9 @@ const defaultValidators: Validators = {
isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey,
isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue,
isValidTag: CommonValidatorFunctions.isValidDnsLabel,
isValidLinkUrl: CommonValidatorFunctions.isValidUrl,
isValidLinkTitle: CommonValidatorFunctions.isValidString,
isValidLinkIcon: KubernetesValidatorFunctions.isValidObjectName,
};
export function makeValidator(overrides: Partial<Validators> = {}): Validators {
@@ -24,4 +24,7 @@ export type Validators = {
isValidAnnotationKey(value: unknown): boolean;
isValidAnnotationValue(value: unknown): boolean;
isValidTag(value: unknown): boolean;
isValidLinkUrl(value: unknown): boolean;
isValidLinkTitle(value: unknown): boolean;
isValidLinkIcon(value: unknown): boolean;
};