import { CompassComponentType, CompassRelationshipType, CustomFieldFromYAML, CustomFieldType, CustomFields, } from '@atlassian/forge-graphql-types'; import { MISSING_CUSTOM_FIELDS_IN_COMPONENT } from '../../requests/config-as-code-requests/helpers/validate-config-file/models/error-messages'; import { YamlFields, YamlLink, YamlRelationships } from '../../types'; import { relationshipKeyTypes, types, validLifecycleValues, } from '../../requests/config-as-code-requests/helpers/validate-config-file/models/compass-yaml-types'; import ConfigFileParser from '../../requests/config-as-code-requests/helpers/validate-config-file/config-file-parser'; import { MAX_DESCRIPTION_LENGTH, MAX_NAME_LENGTH, YAML_VERSION, } from '../../helpers/constants'; import { MOCK_COMPONENT_ID, MOCK_CUSTOM_FIELDS, MOCK_CUSTOM_FIELDS_IN_YAML, } from '../fixtures/mocks'; const BASE_CONFIG = { id: 'ari:cloud:compass:122345:component/12345/12345', name: 'Hello world', }; const MOCK_OWNER_ID = 'ari:cloud:teams::team/12345'; describe('ConfigFileParser', () => { let serviceConfigFileParser: ConfigFileParser; let configFileParser: ConfigFileParser; let newComponentTypeParser: ConfigFileParser; beforeEach(() => { serviceConfigFileParser = new ConfigFileParser( CompassComponentType.Service, ); configFileParser = new ConfigFileParser(CompassComponentType.Other); newComponentTypeParser = new ConfigFileParser('CLOUD_RESOURCE'); }); describe('Fields validators', () => { test('does not add error when component has null fields', () => { const config = { ...BASE_CONFIG, fields: null as YamlFields, }; serviceConfigFileParser.validateConfig(config); expect(serviceConfigFileParser.errors).toEqual([]); configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('does not add error when component has empty fields', () => { const config = { ...BASE_CONFIG, fields: {}, }; serviceConfigFileParser.validateConfig(config); expect(serviceConfigFileParser.errors).toEqual([]); configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('does not add error when component has fields with lifecycle', () => { const config = { ...BASE_CONFIG, fields: { lifecycle: [validLifecycleValues.DEPRECATED], }, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('does not add error when component is missing fields', () => { serviceConfigFileParser.validateConfig(BASE_CONFIG); expect(serviceConfigFileParser.errors).toEqual([]); configFileParser.validateConfig(BASE_CONFIG); expect(configFileParser.errors).toEqual([]); }); test('add no errors if service component has fields', () => { const config = { ...BASE_CONFIG, fields: { tier: '4', }, }; serviceConfigFileParser.validateConfig(config); expect(serviceConfigFileParser.errors).toEqual([]); }); test('add no errors if service component has isMonorepoProject', () => { const config = { ...BASE_CONFIG, fields: { isMonorepoProject: false, }, }; serviceConfigFileParser.validateConfig(config); expect(serviceConfigFileParser.errors).toEqual([]); }); test('add no errors if component has no customFields', () => { configFileParser.validateConfig(BASE_CONFIG); expect(configFileParser.errors).toEqual([]); }); test('adds no errors if component has empty customFields', () => { const config = { ...BASE_CONFIG, customFields: [] as CustomFieldFromYAML[], }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('add error if customField type incorrect', () => { const config = { ...BASE_CONFIG, customFields: [ { name: 'textCustomField', type: 'string', value: 'text', }, ], }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"string" is not a valid customField type. The accepted values are: TEXT, NUMBER, BOOLEAN, USER, SINGLE_SELECT, MULTI_SELECT', ]); }); test("does not add error if tier field value isn't a string", () => { const fields = { tier: 4, }; serviceConfigFileParser.validateFieldProperties(fields); expect(serviceConfigFileParser.errors).toEqual([]); }); test('does not add error if lifecycle field value has incorrect capitalization', () => { const fields = { tier: 4, lifecycle: validLifecycleValues.ACTIVE, }; serviceConfigFileParser.validateFieldProperties(fields); expect(serviceConfigFileParser.errors).toEqual([]); }); test('adds error if field key is unrecognized', () => { const fields = { someKey: 4, }; configFileParser.validateFieldProperties(fields); expect(configFileParser.errors).toEqual([ '"someKey" must be one of the following keys: tier, lifecycle, isMonorepoProject', ]); }); test('truncates fields key if over string limit', () => { const fields = { Loremipsumdolorsitametconsectetur: 2, }; configFileParser.validateFieldProperties(fields); expect(configFileParser.errors).toEqual([ '"Loremipsumdolorsitametcon" must be one of the following keys: tier, lifecycle, isMonorepoProject', ]); }); test("adds error when tier value isn't recognized", () => { const fields = { tier: 0, }; serviceConfigFileParser.validateFieldProperties(fields); expect(serviceConfigFileParser.errors).toEqual([ '"tier" must have a value of: 1, 2, 3, 4', ]); }); test("adds error when lifecycle value isn't recognized", () => { const fields = { tier: 1, lifecycle: 'Invalid', }; serviceConfigFileParser.validateFieldProperties(fields); expect(serviceConfigFileParser.errors).toEqual([ '"lifecycle" must have a value of: Pre-release, Active, Deprecated', ]); }); test('adds error if field key is unrecognized in component', () => { const fields = { someKey: 4, }; configFileParser.validateFieldProperties(fields); expect(configFileParser.errors).toEqual([ '"someKey" must be one of the following keys: tier, lifecycle, isMonorepoProject', ]); serviceConfigFileParser.validateFieldProperties(fields); expect(serviceConfigFileParser.errors).toEqual([ '"someKey" must be one of the following keys: tier, lifecycle, isMonorepoProject', ]); }); }); describe('validValueType', () => { test('does not add error when optional property is set to null', () => { const expectedType = types.OPTIONAL_STRING; configFileParser.validValueType(null, expectedType, 'name'); expect(configFileParser.errors).toEqual([]); }); test('does not add error when optional property is set to value with string type', () => { const expectedType = types.OPTIONAL_STRING; configFileParser.validValueType('hello world', expectedType, 'name'); expect(configFileParser.errors).toEqual([]); }); test('adds error when required property is set to null', () => { const expectedType = types.REQUIRED_STRING; configFileParser.validValueType(null, expectedType, 'name'); expect(configFileParser.errors).toEqual([ '"name" must be of type "string"', ]); }); test('does not add error when required string is set to value with string type', () => { const expectedType = types.REQUIRED_STRING; configFileParser.validValueType('hello world', expectedType, 'name'); expect(configFileParser.errors).toEqual([]); }); test('adds error when links is not an array', () => { const config = { ...BASE_CONFIG, links: {}, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"links" must be of type "array"', ]); }); test('adds error when customFields is not an array', () => { const config = { ...BASE_CONFIG, customFields: {}, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"customFields" must be of type "array"', ]); }); test('adds error when labels is not an array', () => { const config = { ...BASE_CONFIG, labels: {}, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"labels" must be of type "array"', ]); }); }); describe('checkForMandatoryKeys', () => { test('adds error if key is mandatory', () => { const actualKeys = ['name']; const expectedObject = { id: types.REQUIRED_STRING, }; configFileParser.checkForMandatoryKeys(actualKeys, expectedObject); expect(configFileParser.errors).toEqual([ '"id" must be included in the configuration file', ]); }); test('does not add error if key not is mandatory', () => { const actualKeys = ['name']; const expectedObject = { id: types.OPTIONAL_STRING, }; configFileParser.checkForMandatoryKeys(actualKeys, expectedObject); expect(configFileParser.errors).toEqual([]); }); }); describe('checkIfKeyIsUnknown', () => { test('adds error if key is not expected', () => { const key = 'foo'; const expectedKeys = ['id', 'description']; configFileParser.checkIfKeyIsUnknown(key, expectedKeys); expect(configFileParser.errors).toEqual([ '"foo" is not a valid property', ]); }); test('does not add error if key is expected', () => { const key = 'id'; const expectedKeys = ['id', 'description']; configFileParser.checkIfKeyIsUnknown(key, expectedKeys); expect(configFileParser.errors).toEqual([]); }); }); describe('validateLinkProperties', () => { test('adds error if link entry is missing mandatory url', () => { const links = [ { type: 'DOCUMENT', }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ 'the "links" property in the configuration file must include "url"', ]); }); test('adds error if link entry is missing mandatory type', () => { const links = [ { url: 'https://atlassian.com', }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ 'the "links" property in the configuration file must include "type"', ]); }); test('adds error if link entry is missing mandatory url and type', () => { const links = [ { name: 'Test link', }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ 'the "links" property in the configuration file must include "type" and "url"', ]); }); test('does not add error when all mandatory keys are defined', () => { const links = [ { type: 'DOCUMENT', url: 'https://atlassian.com', }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([]); }); test('does not add error when all known keys are defined', () => { const links = [ { type: 'DOCUMENT', url: 'https://atlassian.com', name: 'Hello world', }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([]); }); test('adds error when unexpected key is added to entry', () => { const links = [ { type: 'DOCUMENT', url: 'https://atlassian.com', unknownKey: 1, }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ '"unknownKey" must be one of the following keys: type, url, name', ]); }); describe('validate property types', () => { test('adds error when type is not formatted correctly', () => { const links = [ { type: 1, url: 'https://atlassian.com', }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ '"1" is not a valid link type. The accepted values are: ' + 'DOCUMENT, CHAT_CHANNEL, REPOSITORY, PROJECT, DASHBOARD, OTHER_LINK, ON_CALL', ]); }); test('adds error when type is invalid', () => { const links = [ { type: 'UNKNOWN_TYPE', url: 'https://atlassian.com', }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ '"UNKNOWN_TYPE" is not a valid link type. The accepted values are: ' + 'DOCUMENT, CHAT_CHANNEL, REPOSITORY, PROJECT, DASHBOARD, OTHER_LINK, ON_CALL', ]); }); test('when link type is invalid for a service, adds error with accepted types that includes ON_CALL', () => { const links = [ { type: 'UNKNOWN_TYPE', url: 'https://atlassian.com', }, ]; serviceConfigFileParser.validateLinkProperties(links); expect(serviceConfigFileParser.errors).toEqual([ '"UNKNOWN_TYPE" is not a valid link type. The accepted values are: ' + 'DOCUMENT, CHAT_CHANNEL, REPOSITORY, PROJECT, DASHBOARD, OTHER_LINK, ON_CALL', ]); }); test('truncates type when over string limit', () => { const links = [ { type: 'Loremipsumdolorsitametconsectetur', url: 'https://atlassian.com', }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ '"Loremipsumdolorsitametcon" is not a valid link type. The accepted values are: ' + 'DOCUMENT, CHAT_CHANNEL, REPOSITORY, PROJECT, DASHBOARD, OTHER_LINK, ON_CALL', ]); }); test('adds error when url is not formatted correctly', () => { const links = [ { type: 'DOCUMENT', url: {}, }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ '"url" must be of type "string"', ]); }); test('adds error when name is not formatted correctly', () => { const links = [ { type: 'DOCUMENT', url: 'https://atlassian.com', name: 1, }, ]; configFileParser.validateLinkProperties(links); expect(configFileParser.errors).toEqual([ '"name" must be of type "string"', ]); }); test('does not add error when type is ON_CALL and component is a service', () => { const links = [ { type: 'ON_CALL', url: 'https://atlassian.com', }, ]; serviceConfigFileParser.validateLinkProperties(links); expect(serviceConfigFileParser.errors).toEqual([]); }); }); }); describe('validateCustomFieldsProperties', () => { test('adds error if customField with boolean type entry is missing mandatory name and value', () => { const customFields = [ { type: CustomFieldType.BOOLEAN, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ 'the "customFields" property in the configuration file must include "value" and "name"', ]); }); test('adds error if customField entry is missing mandatory name', () => { const customFields = [ { type: CustomFieldType.TEXT, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ 'the "customFields" property in the configuration file must include "name"', ]); }); test('adds error if customField entry is missing mandatory type', () => { const customFields = [ { name: 'textCustomField', }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"undefined" is not a valid customField type. The accepted values are: TEXT, NUMBER, BOOLEAN, USER, SINGLE_SELECT, MULTI_SELECT', ]); }); test("doesn't add error in case when all mandatory keys are defined", () => { const customFields = [ { name: 'textCustomField', type: CustomFieldType.TEXT, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([]); }); test('does not add error when all mandatory keys are defined', () => { const customFields = [ { name: 'textCustomField', type: CustomFieldType.TEXT, value: 'text', }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([]); }); test('adds error when unexpected key is added to entry', () => { const customFields = [ { name: 'textCustomField', type: CustomFieldType.TEXT, value: 'text', unknownKey: 1, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"unknownKey" must be one of the following keys: type, value, name', ]); }); describe('validate property types', () => { test('adds error when type is not formatted correctly', () => { const customFields = [ { type: 'string', name: 'text', value: 'text', }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"string" is not a valid customField type. The accepted values are: TEXT, NUMBER, BOOLEAN, USER, SINGLE_SELECT, MULTI_SELECT', ]); }); test('truncates type when over string limit', () => { const customFields = [ { type: 'Loremipsumdolorsitametconsectetur', name: 'text', value: 'text', }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"Loremipsumdolorsitametcon" is not a valid customField type. ' + 'The accepted values are: TEXT, NUMBER, BOOLEAN, USER, SINGLE_SELECT, MULTI_SELECT', ]); }); test('adds error when value in text customField has invalid type ', () => { const customFields = [ { name: 'text', type: CustomFieldType.TEXT, value: 123, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"value" must be of type "string"', ]); }); test('adds error when value in boolean customField has invalid type ', () => { const customFields = [ { name: 'boolean', type: CustomFieldType.BOOLEAN, value: 123, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"value" must be of type "boolean"', ]); }); test('adds error when value in number customField has invalid type ', () => { const customFields = [ { name: 'number', type: CustomFieldType.NUMBER, value: true, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"value" must be of type "number"', ]); }); test('adds error when value in user customField has invalid type ', () => { const customFields = [ { name: 'user', type: CustomFieldType.USER, value: true, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"value" must be of type "ARI"', ]); }); test('adds error when name has invalid type', () => { const customFields = [ { name: 4, type: CustomFieldType.BOOLEAN, value: true, }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"name" must be of type "string"', ]); }); test('adds error when value in single select customField has invalid type', () => { const customFields = [ { name: 'single_select1', type: CustomFieldType.SINGLE_SELECT, value: 'asd', }, { name: 'single_select2', type: CustomFieldType.SINGLE_SELECT, value: '', }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ 'option id: "asd" must be of type "UUID"', 'option id: "" must be of type "UUID"', ]); }); test('adds error when displayValue in single select customField has invalid type', () => { const customFields = [ { name: 'single_select1', type: CustomFieldType.SINGLE_SELECT, displayValue: 123, }, { name: 'single_select2', type: CustomFieldType.SINGLE_SELECT, displayValue: ['asd'], }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"displayValue" must be of type "string"', '"displayValue" must be of type "string"', ]); }); test('adds error when value in multi select customField has invalid type', () => { const mockValidUUID = '9463ecdd-20f3-46e1-9642-ff4ca93e0161'; const customFields = [ { name: 'multi-select1', type: CustomFieldType.MULTI_SELECT, value: ['asd', mockValidUUID], }, { name: 'multi-select2', type: CustomFieldType.MULTI_SELECT, value: [mockValidUUID, ''], }, { name: 'multi-select3', type: CustomFieldType.MULTI_SELECT, value: [], }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ 'option id: "asd" must be of type "UUID"', 'option id: "" must be of type "UUID"', ]); }); test('adds error when displayValue in multi select customField has invalid type', () => { const customFields = [ { name: 'multi-select1', type: CustomFieldType.MULTI_SELECT, displayValue: [123], }, { name: 'multi-select2', type: CustomFieldType.MULTI_SELECT, displayValue: ['asd'], }, { name: 'multi-select3', type: CustomFieldType.MULTI_SELECT, displayValue: [], }, ]; configFileParser.validateCustomFieldsProperties(customFields); expect(configFileParser.errors).toEqual([ '"displayValue" must be of type "string"', ]); }); }); }); describe('validate relationship properties', () => { test('does not add error if relationships is missing DEPENDS_ON', () => { const relationships = {}; configFileParser.validateTopLevelProperties( relationships, relationshipKeyTypes, ); expect(configFileParser.errors).toEqual([]); }); test('does not add error if DEPENDS_ON exists', () => { const relationships = { DEPENDS_ON: [], } as YamlRelationships; configFileParser.validateTopLevelProperties( relationships, relationshipKeyTypes, ); expect(configFileParser.errors).toEqual([]); }); test('adds error when unexpected key is added to relationships object', () => { const relationships = { DEPENDS_ON: [], unknownKey: {}, } as YamlRelationships; configFileParser.validateTopLevelProperties( relationships, relationshipKeyTypes, ); expect(configFileParser.errors).toEqual([ '"unknownKey" is not a valid property', ]); }); test('adds error if DEPENDS_ON value is not an object', () => { const relationships = { DEPENDS_ON: 1, }; configFileParser.validateTopLevelProperties( relationships, relationshipKeyTypes, ); expect(configFileParser.errors).toEqual([ '"DEPENDS_ON" must be of type "object"', ]); }); }); describe('validateRelationshipsArray', () => { test('adds error if DEPENDS_ON value is not an object ', () => { configFileParser.validateRelationshipsArray( 1 as unknown as string[], CompassRelationshipType.DependsOn, ); expect(configFileParser.errors).toEqual([]); }); test("adds error if elements in DEPENDS_ON array aren't ARIs or valid slugs", () => { const endNodes = [ 'string:that:is:not:an:ari', 'slug@with!invalid#characters', ' ', 'slug_too_long_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', ]; configFileParser.validateRelationshipsArray( endNodes, CompassRelationshipType.DependsOn, ); expect(configFileParser.errors).toEqual([ '"DEPENDS_ON elements" must be of type "ARI"', '"DEPENDS_ON elements" must be of type "ARI"', '"DEPENDS_ON elements" must be of type "ARI"', '"DEPENDS_ON elements" must be of type "ARI"', ]); }); test('does not add error if elements in DEPENDS_ON array are ARIs ', () => { const endNodes = [MOCK_COMPONENT_ID]; configFileParser.validateRelationshipsArray( endNodes, CompassRelationshipType.DependsOn, ); expect(configFileParser.errors).toEqual([]); }); test('does not add error if DEPENDS_ON is null', () => { configFileParser.validateRelationshipsArray( null, CompassRelationshipType.DependsOn, ); expect(configFileParser.errors).toEqual([]); }); test('does not add error if DEPENDS_ON is empty', () => { configFileParser.validateRelationshipsArray( [], CompassRelationshipType.DependsOn, ); expect(configFileParser.errors).toEqual([]); }); test('does not add error if DEPENDS_ON contains valid slugs and ARIs', () => { const endNodes = ['mock-component-slug-1', MOCK_COMPONENT_ID]; configFileParser.validateRelationshipsArray( endNodes, CompassRelationshipType.DependsOn, ); expect(configFileParser.errors).toEqual([]); }); }); describe('validateConfig', () => { test('adds error when typeId is invalid', () => { const config = { ...BASE_CONFIG, typeId: ' ', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"typeId" must be one of these built-in types or a custom type ARI: SERVICE, LIBRARY, APPLICATION, CAPABILITY, CLOUD_RESOURCE, DATA_PIPELINE, MACHINE_LEARNING_MODEL, UI_ELEMENT, WEBSITE, DATASET, DATA_PRODUCT, DASHBOARD, OTHER', ]); }); test('does not add error when typeId is valid built-in type', () => { const config = { ...BASE_CONFIG, typeId: 'Service', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('does not add error when typeId is valid custom ARI', () => { const config = { ...BASE_CONFIG, typeId: 'ari:cloud:compass:abc:component-type/def/xyz', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('does not add error when typeId could be a valid component type name', () => { const config = { ...BASE_CONFIG, typeId: 'Load Balancer', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('adds error when name is missing', () => { const config = { id: BASE_CONFIG.id, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"name" must be included in the configuration file', ]); }); test('adds error when unexpected key is added to config file', () => { const config = { ...BASE_CONFIG, unknownKey: '', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"unknownKey" is not a valid property', ]); }); test('does not add error when all mandatory properties exist', () => { configFileParser.validateConfig(BASE_CONFIG); expect(configFileParser.errors).toEqual([]); }); describe('validate property types', () => { test('adds error if id value is not an ari', () => { const config = { id: 'somestring', name: 'Hello world', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual(['"id" must be of type "ARI"']); }); test('adds error if name value is not a string', () => { const config = { ...BASE_CONFIG, name: {}, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"name" must be of type "string"', ]); }); test('does not add error if name value is max length', () => { const config = { ...BASE_CONFIG, name: 'a'.repeat(MAX_NAME_LENGTH), }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('adds error if name value is too long', () => { const config = { ...BASE_CONFIG, name: 'a'.repeat(MAX_NAME_LENGTH + 1), }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ `"name" field is too long. Try again with a value no longer than ${MAX_NAME_LENGTH} characters.`, ]); }); test('does not add error if name value is blank', () => { const config = { ...BASE_CONFIG, name: ' ', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('adds error if description value is not a string', () => { const config = { ...BASE_CONFIG, description: 1, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"description" must be of type "string"', ]); }); test('does not add error if description value is max length', () => { const config = { ...BASE_CONFIG, description: 'a'.repeat(MAX_DESCRIPTION_LENGTH), }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('adds error if description value is too long', () => { const config = { ...BASE_CONFIG, description: 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1), }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ `"description" field is too long. Try again with a value no longer than ${MAX_DESCRIPTION_LENGTH} characters.`, ]); }); test('adds error if ownerId value is not a string', () => { const config = { ...BASE_CONFIG, ownerId: {}, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"ownerId" must be of type "string"', ]); }); test('adds error if fields value is not an object', () => { const config = { ...BASE_CONFIG, fields: 'invalid field string', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"fields" must be of type "object"', ]); }); test('adds error if fields value is not an object and does not include tier', () => { const config = { ...BASE_CONFIG, fields: 'invalid field string', }; serviceConfigFileParser.validateConfig(config); expect(serviceConfigFileParser.errors).toEqual([ '"fields" must be of type "object"', ]); }); test('adds error if fields value is an array', () => { const config = { ...BASE_CONFIG, fields: ['tier'], }; serviceConfigFileParser.validateConfig(config); expect(serviceConfigFileParser.errors).toEqual([ '"fields" must be of type "object"', ]); }); test('adds error if links value is not an object', () => { const config = { ...BASE_CONFIG, links: 1, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"links" must be of type "object"', ]); }); test('adds error if labels value is not an object', () => { const config = { ...BASE_CONFIG, labels: 1, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"labels" must be of type "object"', ]); }); test('does not add error if all types are valid', () => { const config = { ...BASE_CONFIG, description: 'Test component', ownerId: MOCK_OWNER_ID, fields: [] as YamlFields, links: [] as YamlLink[], labels: [] as string[], relationships: [] as YamlRelationships, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('does not add error if optional property is null', () => { const config = { ...BASE_CONFIG, links: null as YamlLink, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('adds error if relationships value is not an object', () => { const config = { ...BASE_CONFIG, relationships: 'invalid relationships string', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"relationships" must be of type "object"', ]); }); test('adds error if relationships value is an array', () => { const config = { ...BASE_CONFIG, relationships: ['test'], }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ '"relationships" must be of type "object"', ]); }); test('adds error when configVersion has not default value', () => { const config = { ...BASE_CONFIG, configVersion: 2, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ `"configVersion" is invalid. "configVersion" must be ${YAML_VERSION}`, ]); }); test('adds error when configVersion has string value', () => { const config = { ...BASE_CONFIG, configVersion: 'second', }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([ `"configVersion" is invalid. "configVersion" must be ${YAML_VERSION}`, ]); }); test('does not add error if all types are valid', () => { const config: any = { ...BASE_CONFIG, description: 'Test component', ownerId: MOCK_OWNER_ID, fields: [], links: [], relationships: [], labels: [], configVersion: YAML_VERSION, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); test('does not add error if optional property is null', () => { const config: any = { ...BASE_CONFIG, description: 'Test component', fields: null, links: [], relationships: [], configVersion: null, }; configFileParser.validateConfig(config); expect(configFileParser.errors).toEqual([]); }); }); }); describe('validateLabels', () => { test('adds error if labels item is too long', () => { const maxLabelLength = 1; configFileParser.validateLabels(['label1'], maxLabelLength); expect(configFileParser.errors).toEqual([ `"label" field is too long. Try again with a value no longer than ${maxLabelLength} characters.`, ]); }); test('adds error if labels item contains whitespace characters', () => { const labelWithWhitespace = 'label 1'; configFileParser.validateLabels([labelWithWhitespace], 50); expect(configFileParser.errors).toEqual([ `The component label name [${labelWithWhitespace}] ` + 'contains whitespace characters. Remove all whitespace characters and try again.', ]); }); test('adds error if labels item contains uppercase characters', () => { const labelWithUppercase = 'Label1'; configFileParser.validateLabels([labelWithUppercase], 50); expect(configFileParser.errors).toEqual([ `The component label name [${labelWithUppercase}] ` + 'must be in lowercase. Remove all uppercase characters and try again.', ]); }); }); describe('validateConfigCustomFieldsAgainstComponent', () => { test("add error when component doesn't have customField field, but in yaml file customFields alredy exists", () => { const yamlCustomFields = MOCK_CUSTOM_FIELDS_IN_YAML; configFileParser.validateConfigCustomFieldsAgainstComponent( yamlCustomFields, ); expect(configFileParser.errors).toEqual([ MISSING_CUSTOM_FIELDS_IN_COMPONENT, ]); }); test("add error when component doesn't have the same customField like in yaml file", () => { const yamlCustomFields = MOCK_CUSTOM_FIELDS_IN_YAML; const componentCustomFields = [] as CustomFields; configFileParser.validateConfigCustomFieldsAgainstComponent( yamlCustomFields, componentCustomFields, ); expect(configFileParser.errors).toEqual([ 'Component doesn\'t have custom field with name "testTextFieldName"', 'Component doesn\'t have custom field with name "testBooleanFieldName"', 'Component doesn\'t have custom field with name "testNumberFieldName"', 'Component doesn\'t have custom field with name "testUserFieldName"', 'Component doesn\'t have custom field with name "testSingleSelectFieldName"', 'Component doesn\'t have custom field with name "testMultiSelectFieldName"', ]); }); test('add error when the same customField has a difference in type', () => { const differentYamlCustomField = { name: 'testTextFieldName', type: CustomFieldType.NUMBER, value: 'test custom field value', }; const yamlCustomFields = [...MOCK_CUSTOM_FIELDS_IN_YAML]; yamlCustomFields.splice(0, 1, differentYamlCustomField); const componentCustomFields = MOCK_CUSTOM_FIELDS; configFileParser.validateConfigCustomFieldsAgainstComponent( yamlCustomFields, componentCustomFields, ); expect(configFileParser.errors).toEqual([ '"testTextFieldName" field has a difference in the type. Type must be "text"', ]); }); test('does not add an error when all fields are correct', () => { const yamlCustomFields = MOCK_CUSTOM_FIELDS_IN_YAML; const componentCustomFields = MOCK_CUSTOM_FIELDS; configFileParser.validateConfigCustomFieldsAgainstComponent( yamlCustomFields, componentCustomFields, ); expect(configFileParser.errors).toEqual([]); }); }); describe('link type validation for different componenet types', () => { test('The new component types support oncall links', () => { const links = [ { type: 'ON_CALL', url: 'https://atlassian.com', name: 'Hello world', }, ]; newComponentTypeParser.validateLinkProperties(links); expect(newComponentTypeParser.errors).toEqual([]); }); test('The new component types will still flag invalid link types', () => { const links = [ { type: 'RANDOM', url: 'https://atlassian.com', name: 'Hello world', }, ]; newComponentTypeParser.validateLinkProperties(links); expect(newComponentTypeParser.errors).not.toEqual([]); }); test('A new component type will not bomb out everything and considers oncall valid', () => { const links = [ { type: 'ON_CALL', url: 'https://atlassian.com', name: 'Hello world', }, ]; const unrecognizedTypeParser = new ConfigFileParser( 'UNRECOCNIZED_COMPONENT_TYPE', ); unrecognizedTypeParser.validateLinkProperties(links); expect(unrecognizedTypeParser.errors).toEqual([]); }); }); });